Manual navigation in UI framework and WIP mod menu navigation

This commit is contained in:
Mr-Wiseguy 2025-04-16 16:15:37 -04:00
parent 6166fffc99
commit afc880521c
23 changed files with 128 additions and 12 deletions

View file

@ -466,6 +466,8 @@ recompui::Style* recompui::ContextId::add_resource_impl(std::unique_ptr<Style>&&
auto key = opened_context->resources.emplace(std::move(resource));
if (is_element) {
Element* element_ptr = static_cast<Element*>(resource_ptr);
element_ptr->set_id(std::string{element_ptr->get_type_name()} + "-" + std::to_string(key.raw));
key.set_tag(static_cast<uint8_t>(SlotTag::Element));
// Send one update to the element.
opened_context->to_update.emplace(ResourceId{ key.raw });

View file

@ -20,6 +20,7 @@ namespace recompui {
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Button"; }
public:
Button(Element *parent, const std::string &text, ButtonStyle style);
void add_pressed_callback(std::function<void()> callback);

View file

@ -11,6 +11,7 @@ namespace recompui {
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Clickable"; }
public:
Clickable(Element *parent, bool draggable = false);
void add_pressed_callback(std::function<void(float, float)> callback);

View file

@ -5,6 +5,8 @@
namespace recompui {
class Container : public Element {
protected:
std::string_view get_type_name() override { return "Container"; }
public:
Container(Element* parent, FlexDirection direction, JustifyContent justify_content);
};

View file

@ -147,6 +147,11 @@ void Element::handle_event(const Event& event) {
process_event(event);
}
void Element::set_id(const std::string& new_id) {
id = new_id;
base->SetId(new_id);
}
void Element::ProcessEvent(Rml::Event &event) {
ContextId prev_context = recompui::try_close_current_context();
ContextId context = ContextId::null();

View file

@ -33,6 +33,7 @@ private:
std::unordered_multimap<std::string_view, uint32_t> style_name_index_map;
std::vector<UICallback> callbacks;
std::vector<Element *> children;
std::string id;
bool shim = false;
bool enabled = true;
bool disabled_attribute = false;
@ -44,6 +45,7 @@ private:
void apply_style(Style *style);
void propagate_disabled(bool disabled);
void handle_event(const Event &e);
void set_id(const std::string& new_id);
// Style overrides.
virtual void set_property(Rml::PropertyId property_id, const Rml::Property &property) override;
@ -56,6 +58,7 @@ protected:
virtual void process_event(const Event &e);
virtual ElementValue get_element_value() { return std::monostate{}; }
virtual void set_input_value(const ElementValue&) {}
virtual std::string_view get_type_name() { return "Element"; }
public:
// Used for backwards compatibility with legacy UI elements.
Element(Rml::Element *base);
@ -94,6 +97,7 @@ public:
void set_input_value_u32(uint32_t val) { set_input_value(val); }
void set_input_value_float(float val) { set_input_value(val); }
void set_input_value_double(double val) { set_input_value(val); }
const std::string& get_id() { return id; }
};
void queue_ui_callback(recompui::ResourceId resource, const Event& e, const UICallback& callback);

View file

@ -5,6 +5,8 @@
namespace recompui {
class Image : public Element {
protected:
std::string_view get_type_name() override { return "ImageView"; }
public:
Image(Element *parent, std::string_view src);
};

View file

@ -12,6 +12,8 @@ namespace recompui {
};
class Label : public Element {
protected:
std::string_view get_type_name() override { return "Label"; }
public:
Label(Element *parent, LabelStyle label_style);
Label(Element *parent, const std::string &text, LabelStyle label_style);

View file

@ -13,6 +13,7 @@ namespace recompui {
uint32_t index = 0;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "LabelRadioOption"; }
public:
RadioOption(Element *parent, std::string_view name, uint32_t index);
void set_pressed_callback(std::function<void(uint32_t)> callback);
@ -29,6 +30,8 @@ namespace recompui {
void option_selected(uint32_t index);
void set_input_value(const ElementValue& val) override;
ElementValue get_element_value() override { return get_index(); }
protected:
std::string_view get_type_name() override { return "LabelRadio"; }
public:
Radio(Element *parent);
virtual ~Radio();

View file

@ -10,6 +10,8 @@ namespace recompui {
};
class ScrollContainer : public Element {
protected:
std::string_view get_type_name() override { return "ScrollContainer"; }
public:
ScrollContainer(Element *parent, ScrollDirection direction);
};

View file

@ -36,6 +36,7 @@ namespace recompui {
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Slider"; }
public:
Slider(Element *parent, SliderType type);

View file

@ -6,6 +6,8 @@
namespace recompui {
class Span : public Element {
protected:
std::string_view get_type_name() override { return "Span"; }
public:
Span(Element *parent);
Span(Element *parent, const std::string &text);

View file

@ -1,4 +1,5 @@
#include "ui_style.h"
#include "ui_element.h"
#include <cassert>
@ -587,6 +588,14 @@ namespace recompui {
set_property(nav_to_property(dir), Rml::Style::Nav::None);
}
void Style::set_nav(NavDirection dir, Element* element) {
set_property(nav_to_property(dir), Rml::Property(Rml::String{ "#" + element->get_id() }, Rml::Unit::STRING));
}
void Style::set_nav_manual(NavDirection dir, const std::string& target) {
set_property(nav_to_property(dir), Rml::Property(target, Rml::Unit::STRING));
}
void Style::set_tab_index_auto() {
set_property(Rml::PropertyId::TabIndex, Rml::Style::Nav::Auto);
}

View file

@ -94,9 +94,10 @@ namespace recompui {
void set_drag(Drag drag);
void set_tab_index(TabIndex focus);
void set_font_family(std::string_view family);
// TODO set_nav with Element*
void set_nav_auto(NavDirection dir);
void set_nav_none(NavDirection dir);
void set_nav(NavDirection dir, Element* element);
void set_nav_manual(NavDirection dir, const std::string& target);
void set_tab_index_auto();
void set_tab_index_none();
virtual bool is_element() { return false; }

View file

@ -10,6 +10,7 @@ namespace recompui {
std::vector<std::function<void(const std::string &)>> text_changed_callbacks;
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "TextInput"; }
public:
TextInput(Element *parent, bool text_visible = true);
void set_text(std::string_view text);

View file

@ -6,9 +6,9 @@
namespace recompui {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button") {
Toggle::Toggle(Element *parent) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "button") {
enable_focus();
set_width(162.0f);
set_height(72.0f);
set_border_radius(36.0f);
@ -20,13 +20,19 @@ namespace recompui {
checked_style.set_border_color(Color{ 34, 177, 76, 255 });
hover_style.set_border_color(Color{ 177, 76, 34, 255 });
hover_style.set_background_color(Color{ 206, 120, 68, 76 });
focus_style.set_border_color(Color{ 177, 76, 34, 255 });
focus_style.set_background_color(Color{ 206, 120, 68, 76 });
checked_hover_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_hover_style.set_background_color(Color{ 68, 206, 120, 76 });
checked_focus_style.set_border_color(Color{ 34, 177, 76, 255 });
checked_focus_style.set_background_color(Color{ 68, 206, 120, 76 });
disabled_style.set_border_color(Color{ 177, 76, 34, 128 });
checked_disabled_style.set_border_color(Color{ 34, 177, 76, 128 });
add_style(&checked_style, checked_state);
add_style(&hover_style, hover_state);
add_style(&focus_style, focus_state);
add_style(&checked_hover_style, { checked_state, hover_state });
add_style(&checked_focus_style, { checked_state, focus_state });
add_style(&disabled_style, disabled_state);
add_style(&checked_disabled_style, { checked_state, disabled_state });
@ -92,6 +98,11 @@ namespace recompui {
floater->set_style_enabled(hover_state, hover_active);
break;
}
case EventType::Focus: {
bool focus_active = std::get<EventFocus>(e.variant).active;
set_style_enabled(focus_state, focus_active);
break;
}
case EventType::Enable: {
bool enable_active = std::get<EventEnable>(e.variant).active;
set_style_enabled(disabled_state, !enable_active);

View file

@ -12,7 +12,9 @@ namespace recompui {
std::list<std::function<void(bool)>> checked_callbacks;
Style checked_style;
Style hover_style;
Style focus_style;
Style checked_hover_style;
Style checked_focus_style;
Style disabled_style;
Style checked_disabled_style;
Style floater_checked_style;
@ -25,6 +27,7 @@ namespace recompui {
// Element overrides.
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "Toggle"; }
public:
Toggle(Element *parent);
void set_checked(bool checked);

View file

@ -36,8 +36,8 @@ ConfigOptionElement::~ConfigOptionElement() {
}
void ConfigOptionElement::set_id(std::string_view id) {
this->id = id;
void ConfigOptionElement::set_option_id(std::string_view id) {
this->option_id = id;
}
void ConfigOptionElement::set_name(std::string_view name) {
@ -60,7 +60,7 @@ const std::string &ConfigOptionElement::get_description() const {
// ConfigOptionSlider
void ConfigOptionSlider::slider_value_changed(double v) {
callback(id, v);
callback(option_id, v);
}
ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback) : ConfigOptionElement(parent) {
@ -78,7 +78,7 @@ ConfigOptionSlider::ConfigOptionSlider(Element *parent, double value, double min
// ConfigOptionTextInput
void ConfigOptionTextInput::text_changed(const std::string &text) {
callback(id, text);
callback(option_id, text);
}
ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback) : ConfigOptionElement(parent) {
@ -93,7 +93,7 @@ ConfigOptionTextInput::ConfigOptionTextInput(Element *parent, std::string_view v
// ConfigOptionRadio
void ConfigOptionRadio::index_changed(uint32_t index) {
callback(id, index);
callback(option_id, index);
}
ConfigOptionRadio::ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback) : ConfigOptionElement(parent) {
@ -189,7 +189,7 @@ void ConfigSubMenu::clear_options() {
}
void ConfigSubMenu::add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description) {
option->set_id(id);
option->set_option_id(id);
option->set_name(name);
option->set_description(description);
option->set_hover_callback([this](ConfigOptionElement *option, bool active){ option_hovered(option, active); });

View file

@ -16,16 +16,17 @@ namespace recompui {
class ConfigOptionElement : public Element {
protected:
Label *name_label = nullptr;
std::string id;
std::string option_id;
std::string name;
std::string description;
std::function<void(ConfigOptionElement *, bool)> hover_callback = nullptr;
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ConfigOptionElement"; }
public:
ConfigOptionElement(Element *parent);
virtual ~ConfigOptionElement();
void set_id(std::string_view id);
void set_option_id(std::string_view id);
void set_name(std::string_view name);
void set_description(std::string_view description);
void set_hover_callback(std::function<void(ConfigOptionElement *, bool)> callback);
@ -38,6 +39,7 @@ protected:
std::function<void(const std::string &, double)> callback;
void slider_value_changed(double v);
std::string_view get_type_name() override { return "ConfigOptionSlider"; }
public:
ConfigOptionSlider(Element *parent, double value, double min_value, double max_value, double step_value, bool percent, std::function<void(const std::string &, double)> callback);
};
@ -48,6 +50,7 @@ protected:
std::function<void(const std::string &, const std::string &)> callback;
void text_changed(const std::string &text);
std::string_view get_type_name() override { return "ConfigOptionTextInput"; }
public:
ConfigOptionTextInput(Element *parent, std::string_view value, std::function<void(const std::string &, const std::string &)> callback);
};
@ -58,6 +61,7 @@ protected:
std::function<void(const std::string &, uint32_t)> callback;
void index_changed(uint32_t index);
std::string_view get_type_name() override { return "ConfigOptionRadio"; }
public:
ConfigOptionRadio(Element *parent, uint32_t value, const std::vector<std::string> &options, std::function<void(const std::string &, uint32_t)> callback);
};
@ -77,7 +81,8 @@ private:
void back_button_pressed();
void option_hovered(ConfigOptionElement *option, bool active);
void add_option(ConfigOptionElement *option, std::string_view id, std::string_view name, std::string_view description);
protected:
std::string_view get_type_name() override { return "ConfigSubMenu"; }
public:
ConfigSubMenu(Element *parent);
virtual ~ConfigSubMenu();

View file

@ -4,6 +4,8 @@
namespace recompui {
extern const std::string mod_tab_id;
ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
set_flex(1.0f, 1.0f, 200.0f);
set_height(100.0f, Unit::Percent);
@ -63,13 +65,16 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
{
enable_toggle = context.create_element<Toggle>(enable_container);
enable_toggle->add_checked_callback([this](bool checked){ enable_toggle_checked(checked); });
enable_toggle->set_nav_manual(NavDirection::Up, mod_tab_id);
enable_label = context.create_element<Label>(enable_container, "A currently enabled mod requires this mod", LabelStyle::Annotation);
}
configure_button = context.create_element<Button>(buttons_container, "Configure", recompui::ButtonStyle::Secondary);
configure_button->add_pressed_callback([this](){ configure_button_pressed(); });
configure_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
clear_mod_navigation();
}
ModDetailsPanel::~ModDetailsPanel() {
@ -110,6 +115,14 @@ void ModDetailsPanel::set_mod_configure_pressed_callback(std::function<void()> c
mod_configure_pressed_callback = callback;
}
void ModDetailsPanel::setup_mod_navigation(Element* nav_target) {
enable_toggle->set_nav(NavDirection::Left, nav_target);
}
void ModDetailsPanel::clear_mod_navigation() {
enable_toggle->set_nav_none(NavDirection::Left);
}
void ModDetailsPanel::enable_toggle_checked(bool checked) {
if (mod_toggled_callback != nullptr) {
mod_toggled_callback(checked);

View file

@ -18,7 +18,13 @@ public:
void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled);
void set_mod_toggled_callback(std::function<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback);
void setup_mod_navigation(Element* nav_target);
void clear_mod_navigation();
Toggle* get_enable_toggle() { return enable_toggle; }
Button* get_configure_button() { return configure_button; }
void disable_toggle();
protected:
std::string_view get_type_name() override { return "ModDetailsPanel"; }
private:
recomp::mods::ModDetails cur_details;
Container *thumbnail_container = nullptr;

View file

@ -32,6 +32,9 @@ static bool is_mod_enabled_or_auto(const std::string &mod_id) {
constexpr float modEntryHeight = 120.0f;
constexpr float modEntryPadding = 4.0f;
extern const std::string mod_tab_id;
const std::string mod_tab_id = "#tab_mods";
ModEntryView::ModEntryView(Element *parent) : Element(parent) {
ContextId context = get_current_context();
@ -283,6 +286,22 @@ void ModMenu::mod_selected(uint32_t mod_index) {
bool configure_enabled = !config_schema.options.empty();
mod_details_panel->set_mod_details(mod_details[mod_index], thumbnail_src, toggle_checked, toggle_enabled, auto_enabled, configure_enabled);
mod_entry_buttons[active_mod_index]->set_selected(true);
mod_details_panel->setup_mod_navigation(mod_entry_buttons[mod_index]);
if (configure_enabled) {
Button* configure_button = mod_details_panel->get_configure_button();
refresh_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav(NavDirection::Up, configure_button);
}
else if (toggle_enabled) {
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
refresh_button->set_nav(NavDirection::Up, enable_toggle);
mods_folder_button->set_nav(NavDirection::Up, enable_toggle);
}
else {
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
}
}
}
@ -495,6 +514,8 @@ void ModMenu::create_mod_list() {
mod_entry_buttons.clear();
mod_entry_spacers.clear();
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
// Create the child elements for the list scroll.
for (size_t mod_index = 0; mod_index < mod_details.size(); mod_index++) {
const std::vector<char> &thumbnail = recomp::mods::get_mod_thumbnail(mod_details[mod_index].mod_id);
@ -513,6 +534,13 @@ void ModMenu::create_mod_list() {
mod_entry->set_mod_details(mod_details[mod_index]);
mod_entry->set_mod_thumbnail(thumbnail_name);
mod_entry->set_mod_enabled(is_mod_enabled_or_auto(mod_details[mod_index].mod_id));
mod_entry->set_nav(NavDirection::Right, enable_toggle);
if (mod_index == 0) {
mod_entry->set_nav_manual(NavDirection::Up, mod_tab_id);
}
if (mod_index == mod_details.size() - 1) {
mod_entry->set_nav(NavDirection::Down, install_mods_button);
}
mod_entry_buttons.emplace_back(mod_entry);
}
@ -605,17 +633,23 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
footer_container->set_border_bottom_left_radius(16.0f);
footer_container->set_border_bottom_right_radius(16.0f);
{
Toggle* enable_toggle = mod_details_panel->get_enable_toggle();
Button* configure_button = mod_details_panel->get_configure_button();
install_mods_button = context.create_element<Button>(footer_container, "Install Mods", recompui::ButtonStyle::Primary);
install_mods_button->add_pressed_callback([this](){ open_install_dialog(); });
install_mods_button->set_nav_manual(NavDirection::Up, mod_tab_id);
Element* footer_spacer = context.create_element<Element>(footer_container);
footer_spacer->set_flex(1.0f, 0.0f);
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback([this](){ recomp::mods::scan_mods(); refresh_mods(); });
refresh_button->set_nav_manual(NavDirection::Up, mod_tab_id);
mods_folder_button = context.create_element<Button>(footer_container, "Open Mods Folder", recompui::ButtonStyle::Primary);
mods_folder_button->add_pressed_callback([this](){ open_mods_folder(); });
mods_folder_button->set_nav(NavDirection::Up, configure_button);
mods_folder_button->set_nav_manual(NavDirection::Up, mod_tab_id);
} // footer_container
} // this

View file

@ -18,6 +18,8 @@ public:
void set_mod_thumbnail(const std::string &thumbnail);
void set_mod_enabled(bool enabled);
void set_selected(bool selected);
protected:
std::string_view get_type_name() override { return "ModEntryView"; }
private:
Image *thumbnail_image = nullptr;
Element *body_container = nullptr;
@ -40,6 +42,7 @@ public:
void set_selected(bool selected);
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntryButton"; }
private:
uint32_t mod_index = 0;
ModEntryView *view = nullptr;
@ -56,6 +59,7 @@ private:
void check_height_distance();
protected:
virtual void process_event(const Event &e) override;
std::string_view get_type_name() override { return "ModEntrySpacer"; }
public:
ModEntrySpacer(Element *parent);
void set_target_height(float target_height, bool animate_to_target);
@ -66,6 +70,8 @@ public:
ModMenu(Element *parent);
virtual ~ModMenu();
void set_mods_dirty() { mods_dirty = true; }
protected:
std::string_view get_type_name() override { return "ModMenu"; }
private:
void refresh_mods();
void open_mods_folder();