diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 2b572772e5..dca04cb1e9 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -13,6 +13,11 @@ body { color: #E0DBC8; } +button { + cursor: pointer; + focus: auto; +} + .window { max-width: 1088dp; max-height: 768dp; @@ -40,22 +45,25 @@ body { padding: 0 24dp; line-height: 64dp; opacity: 0.25; - tab-index: auto; - nav: horizontal; - focus: auto; + decorator: vertical-gradient(#c2a42d00 #c2a42d00); + transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; } .window .tab-bar .tab.selected { opacity: 1; border-bottom: 4dp #C2A42D; font-effect: glow(0dp 4dp 0dp 4dp black); - decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%); } -.window .tab-bar .tab:focus-visible { +.window .tab-bar .tab:focus-visible, +.window .tab-bar .tab:hover { opacity: 1; font-effect: glow(0dp 4dp 0dp 4dp black); - decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%); + decorator: vertical-gradient(#c2a42d00 #c2a42d26); +} + +.window .tab-bar .tab:active { + decorator: vertical-gradient(#c2a42d10 #c2a42d40); } .window .content { @@ -112,7 +120,7 @@ body { transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; } -.button.active, .button:hover { +.button.active, .button:hover, .button:focus-visible { background-color: rgba(204, 184, 119, 20%); box-shadow: #C2A42D 0 0 0 2dp; } @@ -135,7 +143,7 @@ body { transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; } -.select-button.active, .select-button:hover { +.select-button.active, .select-button:hover, .select-button:focus-visible { background-color: rgba(204, 184, 119, 20%); box-shadow: #C2A42D 0 0 0 2dp; } diff --git a/src/dusk/ui/button.cpp b/src/dusk/ui/button.cpp index d5c731e0f8..832c2a9876 100644 --- a/src/dusk/ui/button.cpp +++ b/src/dusk/ui/button.cpp @@ -24,6 +24,15 @@ Button::Button(Rml::Element* parent, ButtonProps props, const Rml::String& class mProps.onPressed(event); } }); + listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto cmd = map_nav_event(event); + if (cmd == NavCommand::Confirm) { + if (mProps.onPressed) { + mProps.onPressed(event); + } + event.StopPropagation(); + } + }); } void Button::set_text(const Rml::String& text) { diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index b4f6a287a9..fa1895b5ef 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -15,6 +15,20 @@ void Component::update() { } } +bool Component::focus() { + // Can we focus self? + if (mRoot->Focus(true)) { + return true; + } + // Otherwise, try to focus a child + for (const auto& child : mChildren) { + if (child->focus()) { + return true; + } + } + return false; +} + Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) { if (parent == nullptr) { return nullptr; @@ -35,6 +49,15 @@ void Component::listen(Rml::Element* element, Rml::EventId event, std::make_unique(element, event, std::move(callback), capture)); } +bool Component::contains(Rml::Element* element) const { + for (const auto* node = element; node != nullptr; node = node->GetParentNode()) { + if (node == mRoot) { + return true; + } + } + return false; +} + void Component::clear_children() { mChildren.clear(); while (mRoot->GetNumChildren() > 0) { diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp index 9291a25da8..a90aad45af 100644 --- a/src/dusk/ui/component.hpp +++ b/src/dusk/ui/component.hpp @@ -23,15 +23,11 @@ public: Component& operator=(const Component&) = delete; virtual void update(); + virtual bool focus(); void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false); - - Rml::Element* root() const { return mRoot; } - -protected: - static Rml::Element* append(Rml::Element* parent, const Rml::String& tag); - void clear_children(); + bool contains(Rml::Element* element) const; template requires std::is_base_of_v T& add_child(Args&&... args) { @@ -41,6 +37,12 @@ protected: return ref; } + Rml::Element* root() const { return mRoot; } + +protected: + static Rml::Element* append(Rml::Element* parent, const Rml::String& tag); + void clear_children(); + Rml::Element* mRoot = nullptr; std::vector > mChildren; std::vector > mListeners; diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index 56ef554c3c..5a0c8464a4 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -173,7 +173,7 @@ bool handle_editor_action(const Rml::VariantList& arguments) { EditorWindow::EditorWindow() { add_tab("Player Status", [this](Rml::Element* content) { - auto& leftPane = add_child(content); + auto& leftPane = add_child(content, Pane::Direction::Vertical); leftPane.add_section("Player"); leftPane.add_select_button({ .key = "Player Name", @@ -207,7 +207,7 @@ EditorWindow::EditorWindow() { .selected = false, }); - auto& rightPane = add_child(content); + auto& rightPane = add_child(content, Pane::Direction::Vertical); rightPane.add_button({ .text = "Hello, world!", }); diff --git a/src/dusk/ui/event.cpp b/src/dusk/ui/event.cpp index 0135cd56b7..0ab2211645 100644 --- a/src/dusk/ui/event.cpp +++ b/src/dusk/ui/event.cpp @@ -13,6 +13,7 @@ ScopedEventListener::ScopedEventListener( ScopedEventListener::~ScopedEventListener() { if (mElement != nullptr) { mElement->RemoveEventListener(mEvent, this, mCapture); + mElement = nullptr; } } diff --git a/src/dusk/ui/event.hpp b/src/dusk/ui/event.hpp index 27a318de3f..4521fa1ea9 100644 --- a/src/dusk/ui/event.hpp +++ b/src/dusk/ui/event.hpp @@ -14,6 +14,11 @@ public: Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false); ~ScopedEventListener() override; + ScopedEventListener(const ScopedEventListener&) = delete; + ScopedEventListener& operator=(const ScopedEventListener&) = delete; + ScopedEventListener(ScopedEventListener&&) = delete; + ScopedEventListener& operator=(ScopedEventListener&&) = delete; + void ProcessEvent(Rml::Event& event) override; void OnDetach(Rml::Element* element) override; diff --git a/src/dusk/ui/pane.cpp b/src/dusk/ui/pane.cpp index d36d25ee30..592c8f30ee 100644 --- a/src/dusk/ui/pane.cpp +++ b/src/dusk/ui/pane.cpp @@ -5,16 +5,67 @@ namespace dusk::ui { namespace { -Rml::Element* createRoot(Rml::Element* parent) { +Rml::Element* createRoot(Rml::Element* parent, const Rml::String& className) { auto* doc = parent->GetOwnerDocument(); auto elem = doc->CreateElement("div"); - elem->SetClass("pane", true); + elem->SetClass(className, true); return parent->AppendChild(std::move(elem)); } } // namespace -Pane::Pane(Rml::Element* parent) : Component(createRoot(parent)) {} +Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& className) + : Component(createRoot(parent, className)), mDirection(direction) { + listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto cmd = map_nav_event(event); + int direction = 0; + if ((mDirection == Direction::Vertical && cmd == NavCommand::Down) || + (mDirection == Direction::Horizontal && cmd == NavCommand::Right)) + { + direction = 1; + } else if ((mDirection == Direction::Vertical && cmd == NavCommand::Up) || + (mDirection == Direction::Horizontal && cmd == NavCommand::Left)) + { + direction = -1; + } else { + return; + } + auto* target = event.GetTargetElement(); + int focusedChild = -1; + for (size_t i = 0; i < mChildren.size(); ++i) { + if (mChildren[i]->contains(target)) { + focusedChild = i; + break; + } + } + int i = focusedChild + direction; + if (focusedChild == -1) { + // If the container itself is focused and next is pressed, focus the first element + if (direction == 1) { + i = 0; + } else { + // Otherwise, allow event to bubble + return; + } + } + while (i >= 0 && i < static_cast(mChildren.size())) { + if (mChildren[i]->focus()) { + event.StopPropagation(); + break; + } + i += direction; + } + }); +} + +bool Pane::focus() { + for (const auto& child : mChildren) { + if (child->focus()) { + return true; + } + } + return false; +} Rml::Element* Pane::add_section(const Rml::String& text) { auto* elem = append(mRoot, "div"); diff --git a/src/dusk/ui/pane.hpp b/src/dusk/ui/pane.hpp index ac252bf54c..863ad1a87d 100644 --- a/src/dusk/ui/pane.hpp +++ b/src/dusk/ui/pane.hpp @@ -8,7 +8,14 @@ namespace dusk::ui { class Pane : public Component { public: - explicit Pane(Rml::Element* parent); + enum class Direction { + Vertical, + Horizontal, + }; + + explicit Pane(Rml::Element* parent, Direction direction, const Rml::String& className = "pane"); + + bool focus() override; Rml::Element* add_section(const Rml::String& text); Button& add_button(Button::Props props) { return add_child