diff --git a/recompui/src/api/ui_api_events.cpp b/recompui/src/api/ui_api_events.cpp index 8ee95c5..1273331 100644 --- a/recompui/src/api/ui_api_events.cpp +++ b/recompui/src/api/ui_api_events.cpp @@ -23,8 +23,7 @@ #include "librecomp/overlays.hpp" #include "librecomp/helpers.hpp" -// TODO: Forced game includes -#include "../../../../../patches/ui_funcs.h" +#include "event_structs.h" struct QueuedCallback { recompui::ResourceId resource; diff --git a/recompui/src/base/ui_game_option.cpp b/recompui/src/base/ui_game_option.cpp index 13f3f5c..804d57d 100644 --- a/recompui/src/base/ui_game_option.cpp +++ b/recompui/src/base/ui_game_option.cpp @@ -63,6 +63,13 @@ namespace recompui { } void GameOption::process_event(const Event &e) { + if (event_callback != nullptr) { + bool cancel = event_callback(e); + if (cancel) { + return; + } + } + switch (e.type) { case EventType::Click: if (is_enabled()) { @@ -116,4 +123,8 @@ namespace recompui { void GameOption::set_callback(std::function new_callback) { callback = new_callback; } + + void GameOption::set_event_callback(std::function new_callback) { + event_callback = new_callback; + } } diff --git a/recompui/src/base/ui_game_option.h b/recompui/src/base/ui_game_option.h index cfaab26..aa3dfd6 100644 --- a/recompui/src/base/ui_game_option.h +++ b/recompui/src/base/ui_game_option.h @@ -10,6 +10,7 @@ namespace recompui { private: std::string title; std::function callback; + std::function event_callback = nullptr; protected: Label *label; void set_styles(); @@ -19,6 +20,7 @@ namespace recompui { GameOption(ResourceId rid, Element* parent, const std::string& title, std::function callback, GameOptionsMenuLayout layout); void set_title(const std::string& new_title); void set_callback(std::function new_callback); + void set_event_callback(std::function new_callback); Label* get_label() { return label; } // These are public so you can modify them. Style hover_style; diff --git a/recompui/src/composites/ui_mod_menu.cpp b/recompui/src/composites/ui_mod_menu.cpp index 02455fd..38149d7 100644 --- a/recompui/src/composites/ui_mod_menu.cpp +++ b/recompui/src/composites/ui_mod_menu.cpp @@ -766,6 +766,9 @@ void update_mod_list(bool scan_mods) { } void update_game_mod_id(const std::string &game_mod_id) { + if (current_game_mod_id != "" && game_mod_id != current_game_mod_id && mod_menu) { + mod_menu->set_mods_dirty(false); + } current_game_mod_id = game_mod_id; } diff --git a/recompui/src/elements/ui_element.cpp b/recompui/src/elements/ui_element.cpp index 4517152..eb83e96 100644 --- a/recompui/src/elements/ui_element.cpp +++ b/recompui/src/elements/ui_element.cpp @@ -671,11 +671,6 @@ Element *Element::get_nav_parent() { void Element::set_as_navigation_container(NavigationType nav_type) { is_nav_container = true; this->nav_type = nav_type; - - Element *parent_nav = get_nav_parent(); - if (parent_nav != nullptr) { - parent_nav->nav_children.push_back(this); - } } void Element::set_nav_wrapping(bool wrapping) { @@ -710,53 +705,72 @@ Element::CanFocus Element::is_focusable() { return CanFocus::No; } -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); - } - } -} -// Dive into the hierarchy to build a list of focusable elements and navigation containers. +bool Element::is_distant_parent_of(Element *el) { + if (el == nullptr) { + return false; + } + Element *cur_parent = el->parent; + while (cur_parent != nullptr) { + if (this == cur_parent) { + return true; + } + cur_parent = cur_parent->parent; + } + return false; +} + +/** + * - Element should be visible in order for it or any of its children to be considered for navigation. + * - The current focused element is always treated as a valid element. No need to dive into it. If it was focused, + * it is not a nav container. + * - The document calls this function with nav_parent being itself + * - Nav containers' nav_children dont need to be direct descendants, but once you hit a child that: + * - isnt visible + * - focus is NoAndNoChildren + * - is either focusable OR a nav container + * then you stop/continue. if focusable or a nav container, push it to the nav_parent's children + * - One exception, if this element is a parent or distant parent (like mine), then continue diving into that tree. + */ void Element::build_navigation(Element *nav_parent, Element *cur_focus_element) { - if (!base->IsVisible()) { - return; + bool is_visible = base->IsVisible(); + bool is_current_focus = cur_focus_element ? this->id == cur_focus_element->id : false; + bool is_doc = this->get_type_name() == "Document"; + + if (nav_children.size() > 0) { + nav_children.clear(); } - for (auto &child : children) { - if (child == cur_focus_element) { - nav_parent->nav_children.push_back(child); - continue; + Element::CanFocus can_focus = Element::CanFocus::No; + if (!is_doc) { + // Current focused element doesn't need to be visible or + // focusable, it could have changed either way. + if (is_current_focus) { + nav_parent->nav_children.push_back(this); + return; } - if (!child->base->IsVisible() || !child->enabled) { - continue; + can_focus = is_focusable(); + // All other elements that aren't visible should be skipped. + if (can_focus == Element::CanFocus::NoAndNoChildren && !is_distant_parent_of(cur_focus_element)) { + return; } + } - if ((child->is_focusable() == CanFocus::Yes) || child->is_nav_container) { - nav_parent->nav_children.push_back(child); + if (can_focus == Element::CanFocus::Yes) { + nav_parent->nav_children.push_back(this); + } else if (is_nav_container) { + // Add nav children recursively to self. + for (auto &child : children) { + child->build_navigation(this, cur_focus_element); } - - if (child->is_nav_container) { - 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(); - } + // Only give the nav parent this nav container if it has any focusable elements. + if (nav_children.size() > 0) { + nav_parent->nav_children.push_back(this); } - else { + } else { + // This isn't focusable or a nav container, so iter children recursively from the + // current nav_parent. + for (auto &child : children) { child->build_navigation(nav_parent, cur_focus_element); } } diff --git a/recompui/src/elements/ui_element.h b/recompui/src/elements/ui_element.h index 34eec3f..28d3d2a 100644 --- a/recompui/src/elements/ui_element.h +++ b/recompui/src/elements/ui_element.h @@ -96,8 +96,8 @@ class Element : public Style, public Rml::EventListener { 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); + bool is_distant_parent_of(Element *el); 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); diff --git a/recompui/src/elements/ui_modal.cpp b/recompui/src/elements/ui_modal.cpp index b057f46..99c855a 100644 --- a/recompui/src/elements/ui_modal.cpp +++ b/recompui/src/elements/ui_modal.cpp @@ -128,6 +128,8 @@ void TabbedModal::open() { if (tabs != nullptr) { tabs->focus_on_active_tab(); on_tab_change(tabs->get_active_tab()); + refresh_tab = true; + queue_update(); } } @@ -334,12 +336,13 @@ TabbedModal::TabbedModal( void TabbedModal::process_event(const Event &e) { switch (e.type) { case EventType::Update: { - if (previous_tab_index != current_tab_index) { + if (refresh_tab || previous_tab_index != current_tab_index) { body->clear_children(); if (current_tab_index >= 0 && current_tab_index < tab_contexts.size()) { tab_contexts[current_tab_index].create_contents(modal_root_context, body); } previous_tab_index = current_tab_index; + refresh_tab = false; } queue_update(); break; diff --git a/recompui/src/elements/ui_modal.h b/recompui/src/elements/ui_modal.h index d065a14..81f6045 100644 --- a/recompui/src/elements/ui_modal.h +++ b/recompui/src/elements/ui_modal.h @@ -102,6 +102,7 @@ namespace recompui { TabSet *tabs = nullptr; int previous_tab_index = -1; int current_tab_index = -1; + bool refresh_tab = false; virtual void process_event(const Event &e) override; std::string_view get_type_name() override { return "TabbedModal"; } void on_tab_change(int tab_index); diff --git a/recompui/src/elements/ui_style.cpp b/recompui/src/elements/ui_style.cpp index 99c8e61..963956a 100644 --- a/recompui/src/elements/ui_style.cpp +++ b/recompui/src/elements/ui_style.cpp @@ -795,6 +795,135 @@ namespace recompui { set_property(Rml::PropertyId::Focus, focusable ? Rml::Style::Focus::Auto : Rml::Style::Focus::None); } + const std::unordered_map Style::decorator_type_names = { + {Style::DecoratorType::TiledHorizontal , "tiled-horizontal"}, + {Style::DecoratorType::TiledVertical , "tiled-vertical"}, + {Style::DecoratorType::TiledBox , "tiled-box"}, + {Style::DecoratorType::Image , "image"}, + {Style::DecoratorType::NinePatch , "ninepatch"}, + {Style::DecoratorType::Gradient , "gradient"}, + {Style::DecoratorType::HorizontalGradient , "horizontal-gradient"}, + {Style::DecoratorType::VerticalGradient , "vertical-gradient"}, + {Style::DecoratorType::Shader , "shader"}, + {Style::DecoratorType::LinearGradient , "linear-gradient"}, + {Style::DecoratorType::RepeatingLinearGradient , "repeating-linear-gradient"}, + {Style::DecoratorType::RadialGradient , "radial-gradient"}, + {Style::DecoratorType::RepeatingRadialGradient , "repeating-radial-gradient"}, + {Style::DecoratorType::ConicGradient , "conic-gradient"}, + {Style::DecoratorType::RepeatingConicGradient , "repeating-conic-gradient"}, + }; + + Rml::DecoratorsPtr Style::get_existing_decorators() { + if (property_map.find(Rml::PropertyId::Decorator) != property_map.end()) { + auto cur_decorators = property_map[Rml::PropertyId::Decorator].Get(); + if (cur_decorators != nullptr) { + return cur_decorators; + } + } + + return nullptr; + } + + Rml::DecoratorInstancer* Style::get_decorator_instancer(DecoratorType decorator_type) { + std::string decorator_type_name = decorator_type_names.at(decorator_type); + return Rml::Factory::GetDecoratorInstancer(decorator_type_name); + } + + void Style::set_decorator(DecoratorType decorator_type, Rml::PropertyDictionary properties) { + Rml::DecoratorDeclarationList decorators; + Rml::DecoratorsPtr existing_decorator = get_existing_decorators(); + bool found_existing = false; + std::string decorator_type_name = decorator_type_names.at(decorator_type); + + // BoxArea::Padding is rmlui default but we default containers to Border, so matching for now. + Rml::BoxArea paint_area = Rml::BoxArea::Border; + + Rml::DecoratorInstancer* instancer = Rml::Factory::GetDecoratorInstancer(decorator_type_name); + + if (existing_decorator != nullptr) { + auto& existing_decorators = existing_decorator->list; + for (int i = 0; i < existing_decorators.size(); i++) { + if (existing_decorators[i].type == decorator_type_name) { + decorators.list.emplace_back(Rml::DecoratorDeclaration{ + existing_decorators[i].type, + existing_decorators[i].instancer, + std::move(properties), + paint_area + }); + found_existing = true; + } else { + decorators.list.emplace_back(existing_decorators[i]); + } + } + } + + if (!found_existing) { + const Rml::PropertySpecification& specification = instancer->GetPropertySpecification(); + specification.SetPropertyDefaults(properties); + decorators.list.emplace_back(Rml::DecoratorDeclaration{ + decorator_type_name, + instancer, + std::move(properties), + paint_area + }); + } + + set_property( + Rml::PropertyId::Decorator, + Rml::Property( + Rml::Variant(Rml::MakeShared((std::move(decorators)))), + Rml::Unit::DECORATOR + ) + ); + } + + Rml::PropertyDictionary Style::create_decorator_properties( + Style::DecoratorType decorator_type, + std::initializer_list> props_to_set, + bool set_defaults + ) { + Rml::PropertyDictionary properties; + Rml::DecoratorInstancer* instancer = get_decorator_instancer(decorator_type); + const Rml::PropertySpecification &prop_spec = instancer->GetPropertySpecification(); + for (const auto& prop : props_to_set) { + properties.SetProperty(prop_spec.GetProperty(prop.first)->GetId(), prop.second); + } + if (set_defaults) { + prop_spec.SetPropertyDefaults(properties); + } + return properties; + } + + void Style::set_decorator_horizontal_gradient(const Color &color_left, const Color &color_right) { + Rml::PropertyDictionary properties = create_decorator_properties(DecoratorType::HorizontalGradient, { + { "start-color", color_left.to_rml_property() }, + { "stop-color", color_right.to_rml_property() } + }); + + set_decorator(DecoratorType::HorizontalGradient, properties); + } + void Style::set_decorator_horizontal_gradient(recompui::theme::color color_left, recompui::theme::color color_right, int opacity_left, int opacity_right) { + set_decorator_horizontal_gradient( + get_theme_color_with_opacity(color_left, opacity_left), + get_theme_color_with_opacity(color_right, opacity_right) + ); + } + + void Style::set_decorator_vertical_gradient(const Color &color_top, const Color &color_bottom) { + Rml::PropertyDictionary properties = create_decorator_properties(DecoratorType::VerticalGradient, { + { "start-color", color_top.to_rml_property() }, + { "stop-color", color_bottom.to_rml_property() } + }); + + set_decorator(DecoratorType::VerticalGradient, properties); + } + void Style::set_decorator_vertical_gradient(recompui::theme::color color_top, recompui::theme::color color_bottom, int opacity_left, int opacity_right) { + set_decorator_vertical_gradient( + get_theme_color_with_opacity(color_top, opacity_left), + get_theme_color_with_opacity(color_bottom, opacity_right) + ); + } + Rml::TransformPtr Style::get_existing_transform() { if (property_map.find(Rml::PropertyId::Transform) != property_map.end()) { auto curTransform = property_map[Rml::PropertyId::Transform].Get(); diff --git a/recompui/src/elements/ui_style.h b/recompui/src/elements/ui_style.h index 3fe8e04..9da228e 100644 --- a/recompui/src/elements/ui_style.h +++ b/recompui/src/elements/ui_style.h @@ -18,6 +18,50 @@ namespace recompui { private: std::map property_map; Rml::TransformPtr get_existing_transform(); + Rml::DecoratorsPtr get_existing_decorators(); + + // Various ways of applying effects to containers. These are built + // into RmlUI but some require custom implementations. + // It isn't worth exposing all at once, so instead we'll have methods. + // See: https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators.html + enum class DecoratorType { + TiledHorizontal, + TiledVertical, + TiledBox, + Image, + NinePatch, + // (deprecated, use horizontal-gradient or vertical-gradient) + Gradient, + HorizontalGradient, + VerticalGradient, + // Custom shader, see DecoratorShader and https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators/shader.html + // Requires implementation in recompui renderer. Currently not supported in recompui. + Shader, + // Not supported: needs implementation in recompui renderer + LinearGradient, + // Not supported: needs implementation in recompui renderer + RepeatingLinearGradient, + // Not supported: needs implementation in recompui renderer + RadialGradient, + // Not supported: needs implementation in recompui renderer + RepeatingRadialGradient, + // Not supported: needs implementation in recompui renderer + ConicGradient, + // Not supported: needs implementation in recompui renderer + RepeatingConicGradient, + }; + static const std::unordered_map decorator_type_names; + Rml::DecoratorInstancer* get_decorator_instancer(Style::DecoratorType decorator_type); + // Helper to create decorator properties since decorator properties are unique to each decorator type. + // Note: `props_to_set` is a list of property name to rml property pairs, with each pair like: + // `{ "start-color", Rml::Property(Rml::Colourb(r, g, b, a), Rml::Unit::COLOUR) }` + Rml::PropertyDictionary create_decorator_properties( + Style::DecoratorType decorator_type, + std::initializer_list> props_to_set, + bool set_defaults = true + ); + void set_decorator(DecoratorType decorator_type, Rml::PropertyDictionary properties); + void set_or_add_transformation(const Rml::TransformPrimitive& primitive); // Used with display_hide and display_show to restore the intended display value if wanting to @@ -141,6 +185,12 @@ namespace recompui { void set_pointer_events(PointerEvents pointer_events); virtual bool is_element() { return false; } ResourceId get_resource_id() { return resource_id; } + + void set_decorator_horizontal_gradient(const Color &color_left, const Color &color_right); + void set_decorator_horizontal_gradient(recompui::theme::color color_left, recompui::theme::color color_right, int opacity_left = ThemeDefaultOpacity, int opacity_right = ThemeDefaultOpacity); + + void set_decorator_vertical_gradient(const Color &color_top, const Color &color_bottom); + void set_decorator_vertical_gradient(recompui::theme::color color_top, recompui::theme::color color_bottom, int opacity_left = ThemeDefaultOpacity, int opacity_right = ThemeDefaultOpacity); }; } // namespace recompui diff --git a/recompui/src/elements/ui_types.h b/recompui/src/elements/ui_types.h index af97fc4..815e7e7 100644 --- a/recompui/src/elements/ui_types.h +++ b/recompui/src/elements/ui_types.h @@ -5,6 +5,8 @@ #include #include +#include "RmlUi/Core.h" + namespace recompui { constexpr std::string_view checked_state = "checked"; @@ -17,6 +19,10 @@ namespace recompui { uint8_t g = 255; uint8_t b = 255; uint8_t a = 255; + + Rml::Property to_rml_property() const { + return Rml::Property(Rml::Colourb(r, g, b, a), Rml::Unit::COLOUR); + } }; enum class Cursor {