#include "RmlUi/Core/StringUtilities.h" #include "overloaded.h" #include "recomp_ui.h" #include "ui_element.h" #include "../core/ui_context.h" #include namespace recompui { Element::Element(Rml::Element *base) { assert(base != nullptr); this->base = base; this->base_owning = {}; this->shim = true; } Element::Element(Element* parent, uint32_t events_enabled, Rml::String base_class, bool can_set_text) : can_set_text(can_set_text) { ContextId context = get_current_context(); base_owning = context.get_document()->CreateElement(base_class); if (parent != nullptr) { base = parent->base->AppendChild(std::move(base_owning)); parent->add_child(this); } else { base = base_owning.get(); } set_display(Display::Block); set_property(Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox); register_event_listeners(events_enabled); } Element::~Element() { if (!shim) { clear_children(); if (!base_owning) { base->GetParentNode()->RemoveChild(base); } } } void Element::add_child(Element *child) { assert(child != nullptr); if (can_set_text) { assert(false && "Elements with settable text cannot have children"); return; } children.emplace_back(child); if (shim) { ContextId context = get_current_context(); context.add_loose_element(child); } } void Element::set_property(Rml::PropertyId property_id, const Rml::Property &property) { assert(base != nullptr); base->SetProperty(property_id, property); Style::set_property(property_id, property); } void Element::register_event_listeners(uint32_t events_enabled) { assert(base != nullptr); this->events_enabled = events_enabled; if (events_enabled & Events(EventType::Click)) { base->AddEventListener(Rml::EventId::Click, this); } if (events_enabled & Events(EventType::Focus)) { base->AddEventListener(Rml::EventId::Focus, this); base->AddEventListener(Rml::EventId::Blur, this); } if (events_enabled & Events(EventType::Hover)) { base->AddEventListener(Rml::EventId::Mouseover, this); base->AddEventListener(Rml::EventId::Mouseout, this); } if (events_enabled & Events(EventType::Drag)) { base->AddEventListener(Rml::EventId::Drag, this); base->AddEventListener(Rml::EventId::Dragstart, this); base->AddEventListener(Rml::EventId::Dragend, this); } if (events_enabled & Events(EventType::Text)) { base->AddEventListener(Rml::EventId::Change, this); } if (events_enabled & Events(EventType::Navigate)) { base->AddEventListener(Rml::EventId::Keydown, this); } } void Element::apply_style(Style *style) { for (auto it : style->property_map) { // Skip redundant SetProperty calls to prevent dirtying unnecessary state. // This avoids expensive layout operations when a simple color-only style is applied. const Rml::Property* cur_value = base->GetLocalProperty(it.first); if (*cur_value != it.second) { base->SetProperty(it.first, it.second); } } } void Element::apply_styles() { apply_style(this); for (size_t i = 0; i < styles_counter.size(); i++) { if (styles_counter[i] == 0) { apply_style(styles[i]); } } } void Element::propagate_disabled(bool disabled) { disabled_from_parent = disabled; bool attribute_state = disabled_from_parent || !enabled; if (disabled_attribute != attribute_state) { disabled_attribute = attribute_state; base->SetAttribute("disabled", attribute_state); if (events_enabled & Events(EventType::Enable)) { handle_event(Event::enable_event(!attribute_state)); } for (auto &child : children) { child->propagate_disabled(attribute_state); } } } void Element::handle_event(const Event& event) { for (const auto& callback : callbacks) { recompui::queue_ui_callback(resource_id, event, callback); } 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(); Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument(); if (doc != nullptr) { context = get_context_from_document(doc); } bool did_open = false; // TODO disallow null contexts once the entire UI system has been migrated. if (context != ContextId::null()) { did_open = context.open_if_not_already(); } // Events that are processed during any phase. switch (event.GetId()) { case Rml::EventId::Click: handle_event(Event::click_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f))); break; case Rml::EventId::Keydown: switch ((Rml::Input::KeyIdentifier)event.GetParameter("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)); break; } 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; default: break; } // Events that are only processed during the Target phase. if (event.GetPhase() == Rml::EventPhase::Target) { switch (event.GetId()) { case Rml::EventId::Mouseover: handle_event(Event::hover_event(true)); break; case Rml::EventId::Mouseout: handle_event(Event::hover_event(false)); break; case Rml::EventId::Focus: handle_event(Event::focus_event(true)); break; case Rml::EventId::Blur: handle_event(Event::focus_event(false)); break; case Rml::EventId::Dragstart: handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::Start)); break; case Rml::EventId::Dragend: handle_event(Event::drag_event(event.GetParameter("mouse_x", 0.0f), event.GetParameter("mouse_y", 0.0f), DragPhase::End)); break; case Rml::EventId::Change: { if (events_enabled & Events(EventType::Text)) { Rml::Variant *value_variant = base->GetAttribute("value"); if (value_variant != nullptr) { handle_event(Event::text_event(value_variant->Get())); } } break; } default: break; } } if (context != ContextId::null() && did_open) { context.close(); } if (prev_context != ContextId::null()) { prev_context.open(); } } void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) { base->SetAttribute(attribute_key, attribute_value); } void Element::process_event(const Event &) { // Does nothing by default. } void Element::enable_focus() { set_tab_index_auto(); set_focusable(true); set_nav_auto(NavDirection::Up); set_nav_auto(NavDirection::Down); set_nav_auto(NavDirection::Left); set_nav_auto(NavDirection::Right); } void Element::clear_children() { if (children.empty()) { return; } ContextId context = get_current_context(); // Remove the children from the context. for (Element* child : children) { context.destroy_resource(child); } // Clear the child list. children.clear(); } bool Element::remove_child(ResourceId child) { bool found = false; ContextId context = get_current_context(); for (auto it = children.begin(); it != children.end(); ++it) { Element* cur_child = *it; if (cur_child->get_resource_id() == child) { children.erase(it); context.destroy_resource(cur_child); found = true; break; } } return found; } void Element::add_style(Style *style, const std::string_view style_name) { add_style(style, { style_name }); } void Element::add_style(Style *style, const std::initializer_list &style_names) { for (const std::string_view &style_name : style_names) { style_name_index_map.emplace(style_name, styles.size()); } styles.emplace_back(style); uint32_t initial_style_counter = style_names.size(); for (const std::string_view &style_name : style_names) { if (style_active_set.find(style_name) != style_active_set.end()) { initial_style_counter--; } } styles_counter.push_back(initial_style_counter); } void Element::set_enabled(bool enabled) { this->enabled = enabled; propagate_disabled(disabled_from_parent); } bool Element::is_enabled() const { return enabled && !disabled_from_parent; } // Adapted from RmlUi's `EncodeRml`. std::string escape_rml(std::string_view string) { std::string result; result.reserve(string.size()); for (char c : string) { switch (c) { case '<': result += "<"; break; case '>': result += ">"; break; case '&': result += "&"; break; case '"': result += """; break; case '\n': result += "
"; break; default: result += c; break; } } return result; } void Element::set_text(std::string_view text) { if (can_set_text) { // Escape the string into Rml to prevent element injection. base->SetInnerRML(escape_rml(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{}); } void Element::set_input_text(std::string_view val) { base->SetAttribute("value", std::string{ val }); } void Element::set_src(std::string_view src) { base->SetAttribute("src", std::string(src)); } void Element::set_style_enabled(std::string_view style_name, bool enable) { if (enable && style_active_set.find(style_name) == style_active_set.end()) { // Style was disabled and will be enabled. style_active_set.emplace(style_name); } else if (!enable && style_active_set.find(style_name) != style_active_set.end()) { // Style was enabled and will be disabled. style_active_set.erase(style_name); } else { // Do nothing. return; } auto range = style_name_index_map.equal_range(style_name); for (auto it = range.first; it != range.second; it++) { if (enable) { styles_counter[it->second]--; } else { styles_counter[it->second]++; } } apply_styles(); } bool Element::is_style_enabled(std::string_view style_name) { return style_active_set.contains(style_name); } float Element::get_absolute_left() { return base->GetAbsoluteLeft(); } float Element::get_absolute_top() { return base->GetAbsoluteTop(); } float Element::get_client_left() { return base->GetClientLeft(); } float Element::get_client_top() { return base->GetClientTop(); } float Element::get_client_width() { return base->GetClientWidth(); } float Element::get_client_height() { return base->GetClientHeight(); } uint32_t Element::get_input_value_u32() { ElementValue value = get_element_value(); return std::visit(overloaded { [](double d) { return (uint32_t)d; }, [](float f) { return (uint32_t)f; }, [](uint32_t u) { return u; }, [](std::monostate) { return 0U; } }, value); } float Element::get_input_value_float() { ElementValue value = get_element_value(); return std::visit(overloaded { [](double d) { return (float)d; }, [](float f) { return f; }, [](uint32_t u) { return (float)u; }, [](std::monostate) { return 0.0f; } }, value); } double Element::get_input_value_double() { ElementValue value = get_element_value(); return std::visit(overloaded { [](double d) { return d; }, [](float f) { return (double)f; }, [](uint32_t u) { return (double)u; }, [](std::monostate) { return 0.0; } }, value); } void Element::focus() { base->Focus(); } void Element::queue_update() { ContextId cur_context = get_current_context(); // TODO disallow null contexts once the entire UI system has been migrated. if (cur_context == ContextId::null()) { return; } cur_context.queue_element_update(resource_id); } void Element::register_callback(ContextId context, PTR(void) callback, PTR(void) userdata) { callbacks.emplace_back(UICallback{.context = context, .callback = callback, .userdata = userdata}); } }