diff --git a/CMakeLists.txt b/CMakeLists.txt index 2019c8b..2dd58fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,6 +183,7 @@ set (SOURCES ${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp ${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/core/ui_context.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ui_button.cpp diff --git a/src/ui/core/ui_context.cpp b/src/ui/core/ui_context.cpp index 165c382..dbe9b6d 100644 --- a/src/ui/core/ui_context.cpp +++ b/src/ui/core/ui_context.cpp @@ -34,6 +34,7 @@ namespace recompui { resource_slotmap resources; Rml::ElementDocument* document; Element root_element; + Element* autofocus_element = nullptr; std::vector loose_elements; std::unordered_set to_update; bool captures_input = true; @@ -72,6 +73,8 @@ enum class ContextErrorType { DestroyResourceInWrongContext, DestroyResourceNotFound, GetDocumentInvalidContext, + GetAutofocusInvalidContext, + SetAutofocusInvalidContext, InternalError, }; @@ -134,6 +137,12 @@ void context_error(recompui::ContextId id, ContextErrorType type) { case ContextErrorType::GetDocumentInvalidContext: error_message = "Attempted to get the document of an invalid UI context"; break; + case ContextErrorType::GetAutofocusInvalidContext: + error_message = "Attempted to get the autofocus element of an invalid UI context"; + break; + case ContextErrorType::SetAutofocusInvalidContext: + error_message = "Attempted to set the autofocus element of an invalid UI context"; + break; case ContextErrorType::InternalError: error_message = "Internal error in UI context"; break; @@ -572,6 +581,28 @@ recompui::Element* recompui::ContextId::get_root_element() { return &ctx->root_element; } +recompui::Element* recompui::ContextId::get_autofocus_element() { + std::lock_guard lock{ context_state.all_contexts_lock }; + + Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id }); + if (ctx == nullptr) { + context_error(*this, ContextErrorType::GetAutofocusInvalidContext); + } + + return ctx->autofocus_element; +} + +void recompui::ContextId::set_autofocus_element(Element* element) { + std::lock_guard lock{ context_state.all_contexts_lock }; + + Context* ctx = context_state.all_contexts.get(context_slotmap::key{ slot_id }); + if (ctx == nullptr) { + context_error(*this, ContextErrorType::SetAutofocusInvalidContext); + } + + ctx->autofocus_element = element; +} + recompui::ContextId recompui::get_current_context() { // Ensure a context is currently opened by this thread. if (opened_context_id == ContextId::null()) { diff --git a/src/ui/core/ui_context.h b/src/ui/core/ui_context.h index e0d2eaa..a7a4f60 100644 --- a/src/ui/core/ui_context.h +++ b/src/ui/core/ui_context.h @@ -40,6 +40,8 @@ namespace recompui { Rml::ElementDocument* get_document(); Element* get_root_element(); + Element* get_autofocus_element(); + void set_autofocus_element(Element* element); void open(); bool open_if_not_already(); diff --git a/src/ui/elements/ui_button.cpp b/src/ui/elements/ui_button.cpp index 5180009..78a768e 100644 --- a/src/ui/elements/ui_button.cpp +++ b/src/ui/elements/ui_button.cpp @@ -4,9 +4,11 @@ namespace recompui { - Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable), "button", true) { + Button::Button(Element *parent, const std::string &text, ButtonStyle style) : Element(parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus), "button", true) { this->style = style; + enable_focus(); + set_text(text); set_display(Display::Block); set_padding(23.0f); @@ -21,6 +23,7 @@ namespace recompui { set_color(Color{ 204, 204, 204, 255 }); set_tab_index(TabIndex::Auto); hover_style.set_color(Color{ 242, 242, 242, 255 }); + focus_style.set_color(Color{ 242, 242, 242, 255 }); disabled_style.set_color(Color{ 204, 204, 204, 128 }); hover_disabled_style.set_color(Color{ 242, 242, 242, 128 }); @@ -34,6 +37,8 @@ namespace recompui { set_background_color({ 185, 125, 242, background_opacity }); hover_style.set_border_color({ 185, 125, 242, border_hover_opacity }); hover_style.set_background_color({ 185, 125, 242, background_hover_opacity }); + focus_style.set_border_color({ 185, 125, 242, border_hover_opacity }); + focus_style.set_background_color({ 185, 125, 242, background_hover_opacity }); disabled_style.set_border_color({ 185, 125, 242, border_opacity / 4 }); disabled_style.set_background_color({ 185, 125, 242, background_opacity / 4 }); hover_disabled_style.set_border_color({ 185, 125, 242, border_hover_opacity / 4 }); @@ -45,6 +50,8 @@ namespace recompui { set_background_color({ 23, 214, 232, background_opacity }); hover_style.set_border_color({ 23, 214, 232, border_hover_opacity }); hover_style.set_background_color({ 23, 214, 232, background_hover_opacity }); + focus_style.set_border_color({ 23, 214, 232, border_hover_opacity }); + focus_style.set_background_color({ 23, 214, 232, background_hover_opacity }); disabled_style.set_border_color({ 23, 214, 232, border_opacity / 4 }); disabled_style.set_background_color({ 23, 214, 232, background_opacity / 4 }); hover_disabled_style.set_border_color({ 23, 214, 232, border_hover_opacity / 4 }); @@ -57,6 +64,7 @@ namespace recompui { } add_style(&hover_style, hover_state); + add_style(&focus_style, focus_state); add_style(&disabled_style, disabled_state); add_style(&hover_disabled_style, { hover_state, disabled_state }); @@ -78,6 +86,9 @@ namespace recompui { case EventType::Enable: set_style_enabled(disabled_state, !std::get(e.variant).active); break; + case EventType::Focus: + set_style_enabled(focus_state, std::get(e.variant).active); + break; case EventType::Update: break; default: diff --git a/src/ui/elements/ui_button.h b/src/ui/elements/ui_button.h index b5ff114..8c8b44a 100644 --- a/src/ui/elements/ui_button.h +++ b/src/ui/elements/ui_button.h @@ -13,6 +13,7 @@ namespace recompui { protected: ButtonStyle style = ButtonStyle::Primary; Style hover_style; + Style focus_style; Style disabled_style; Style hover_disabled_style; std::list> pressed_callbacks; @@ -23,6 +24,7 @@ namespace recompui { Button(Element *parent, const std::string &text, ButtonStyle style); void add_pressed_callback(std::function callback); Style* get_hover_style() { return &hover_style; } + Style* get_focus_style() { return &focus_style; } Style* get_disabled_style() { return &disabled_style; } Style* get_hover_disabled_style() { return &hover_disabled_style; } }; diff --git a/src/ui/elements/ui_element.cpp b/src/ui/elements/ui_element.cpp index 80b7138..86aacf5 100644 --- a/src/ui/elements/ui_element.cpp +++ b/src/ui/elements/ui_element.cpp @@ -72,7 +72,7 @@ void Element::register_event_listeners(uint32_t events_enabled) { this->events_enabled = events_enabled; if (events_enabled & Events(EventType::Click)) { - base->AddEventListener(Rml::EventId::Mousedown, this); + base->AddEventListener(Rml::EventId::Click, this); } if (events_enabled & Events(EventType::Focus)) { @@ -94,11 +94,20 @@ void Element::register_event_listeners(uint32_t events_enabled) { 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) { - base->SetProperty(it.first, it.second); + // 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); + } } } @@ -155,9 +164,25 @@ void Element::ProcessEvent(Rml::Event &event) { // Events that are processed during any phase. switch (event.GetId()) { - case Rml::EventId::Mousedown: + 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; @@ -218,6 +243,15 @@ void Element::process_event(const Event &) { // Does nothing by default. } +void Element::enable_focus() { + set_property(Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto); + set_property(Rml::PropertyId::Focus, Rml::Style::Focus::Auto); + set_property(Rml::PropertyId::NavUp, Rml::Style::Nav::Auto); + set_property(Rml::PropertyId::NavDown, Rml::Style::Nav::Auto); + set_property(Rml::PropertyId::NavLeft, Rml::Style::Nav::Auto); + set_property(Rml::PropertyId::NavRight, Rml::Style::Nav::Auto); +} + void Element::clear_children() { if (children.empty()) { return; @@ -352,6 +386,10 @@ void Element::set_style_enabled(std::string_view style_name, bool enable) { 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(); } @@ -409,6 +447,10 @@ double Element::get_input_value_double() { }, value); } +void Element::focus() { + base->Focus(); +} + void Element::queue_update() { ContextId cur_context = get_current_context(); diff --git a/src/ui/elements/ui_element.h b/src/ui/elements/ui_element.h index 9484b93..38eb7d9 100644 --- a/src/ui/elements/ui_element.h +++ b/src/ui/elements/ui_element.h @@ -42,7 +42,6 @@ private: void add_child(Element *child); void register_event_listeners(uint32_t events_enabled); void apply_style(Style *style); - void apply_styles(); void propagate_disabled(bool disabled); void handle_event(const Event &e); @@ -76,6 +75,8 @@ public: void set_input_text(std::string_view text); void set_src(std::string_view src); void set_style_enabled(std::string_view style_name, bool enabled); + bool is_style_enabled(std::string_view style_name); + void apply_styles(); bool is_element() override { return true; } float get_absolute_left(); float get_absolute_top(); @@ -83,6 +84,8 @@ public: float get_client_top(); float get_client_width(); float get_client_height(); + void enable_focus(); + void focus(); void queue_update(); void register_callback(ContextId context, PTR(void) callback, PTR(void) userdata); uint32_t get_input_value_u32(); diff --git a/src/ui/elements/ui_radio.cpp b/src/ui/elements/ui_radio.cpp index 5f63730..9128f79 100644 --- a/src/ui/elements/ui_radio.cpp +++ b/src/ui/elements/ui_radio.cpp @@ -1,13 +1,15 @@ #include "overloaded.h" #include "ui_radio.h" +#include "../ui_utils.h" namespace recompui { // RadioOption - RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable), "label", true) { + RadioOption::RadioOption(Element *parent, std::string_view name, uint32_t index) : Element(parent, Events(EventType::Click, EventType::Focus, EventType::Hover, EventType::Enable, EventType::Update), "label", true) { this->index = index; + enable_focus(); set_text(name); set_cursor(Cursor::Pointer); set_font_size(20.0f); @@ -24,9 +26,11 @@ namespace recompui { hover_style.set_color(Color{ 255, 255, 255, 204 }); checked_style.set_color(Color{ 255, 255, 255, 255 }); checked_style.set_border_color(Color{ 242, 242, 242, 255 }); + pulsing_style.set_border_color(Color{ 23, 214, 232, 244 }); add_style(&hover_style, { hover_state }); add_style(&checked_style, { checked_state }); + add_style(&pulsing_style, { focus_state }); } void RadioOption::set_pressed_callback(std::function callback) { @@ -48,6 +52,22 @@ namespace recompui { case EventType::Enable: set_style_enabled(disabled_state, !std::get(e.variant).active); break; + case EventType::Focus: + { + bool active = std::get(e.variant).active; + set_style_enabled(focus_state, active); + if (active) { + queue_update(); + } + } + break; + case EventType::Update: + if (is_style_enabled(focus_state)) { + pulsing_style.set_color(recompui::get_pulse_color(750)); + apply_styles(); + queue_update(); + } + break; default: break; } diff --git a/src/ui/elements/ui_radio.h b/src/ui/elements/ui_radio.h index 8c31415..0a1d218 100644 --- a/src/ui/elements/ui_radio.h +++ b/src/ui/elements/ui_radio.h @@ -8,6 +8,7 @@ namespace recompui { private: Style hover_style; Style checked_style; + Style pulsing_style; std::function pressed_callback = nullptr; uint32_t index = 0; protected: diff --git a/src/ui/elements/ui_slider.cpp b/src/ui/elements/ui_slider.cpp index a3994da..f325956 100644 --- a/src/ui/elements/ui_slider.cpp +++ b/src/ui/elements/ui_slider.cpp @@ -1,5 +1,6 @@ #include "overloaded.h" #include "ui_slider.h" +#include "../ui_utils.h" #include #include @@ -45,7 +46,6 @@ namespace recompui { void Slider::update_circle_position() { double ratio = std::clamp((value - min_value) / (max_value - min_value), 0.0, 1.0); - float slider_relative_left = slider_element->get_absolute_left() - get_absolute_left(); circle_element->set_left(ratio * 100.0, Unit::Percent); } @@ -72,7 +72,42 @@ namespace recompui { }, val); } - Slider::Slider(Element *parent, SliderType type) : Element(parent) { + void Slider::process_event(const Event& e) { + switch (e.type) { + case EventType::Focus: + { + bool active = std::get(e.variant).active; + circle_element->set_style_enabled(focus_state, active); + if (active) { + queue_update(); + } + } + break; + case EventType::Update: + if (circle_element->is_style_enabled(focus_state)) { + circle_element->set_background_color(recompui::get_pulse_color(750)); + queue_update(); + } + else { + circle_element->set_background_color(Color{ 204, 204, 204, 255 }); + } + break; + case EventType::Navigate: + { + NavDirection dir = std::get(e.variant).direction; + if (dir == NavDirection::Left) { + do_step(false); + } + else if (dir == NavDirection::Right) { + do_step(true); + } + } + default: + break; + } + } + + Slider::Slider(Element *parent, SliderType type) : Element(parent, Events(EventType::Focus, EventType::Update, EventType::Navigate)) { this->type = type; set_display(Display::Flex); @@ -80,6 +115,10 @@ namespace recompui { set_text_align(TextAlign::Left); set_min_width(120.0f); + enable_focus(); + set_nav_none(NavDirection::Left); + set_nav_none(NavDirection::Right); + ContextId context = get_current_context(); value_label = context.create_element