diff --git a/files.cmake b/files.cmake index eca3250d01..f0fa42489a 100644 --- a/files.cmake +++ b/files.cmake @@ -1470,16 +1470,21 @@ set(DUSK_FILES src/dusk/ui/editor.hpp src/dusk/ui/event.cpp src/dusk/ui/event.hpp + src/dusk/ui/nav_types.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp src/dusk/ui/pane.cpp src/dusk/ui/pane.hpp + src/dusk/ui/popup.cpp + src/dusk/ui/popup.hpp src/dusk/ui/select_button.cpp src/dusk/ui/select_button.hpp src/dusk/ui/settings.cpp src/dusk/ui/settings.hpp src/dusk/ui/string_button.cpp src/dusk/ui/string_button.hpp + src/dusk/ui/tab_button.cpp + src/dusk/ui/tab_button.hpp src/dusk/ui/ui.hpp src/dusk/ui/ui.cpp src/dusk/ui/window.hpp diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss new file mode 100644 index 0000000000..52c3e42d5d --- /dev/null +++ b/res/rml/popup.rcss @@ -0,0 +1,73 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + color: #E0DBC8; +} + +button { + cursor: pointer; + focus: auto; +} + +.popup { + width: 100%; + display: flex; + align-items: stretch; + width: 100%; + height: 64dp; + background-color: rgba(21, 22, 16, 80%); + border-bottom: 2dp #92875B; + backdrop-filter: blur(5dp); + transform: translateY(0); + transition: transform 0.5s cubic-in-out, opacity 0.5s cubic-in-out; +} + +.popup.popup-hidden { + transform: translateY(-100%); +} + +.popup .tab-bar { + display: flex; + flex: 1 1 0; + min-width: 0; + overflow: auto hidden; + text-transform: uppercase; +} + +.popup .tab-bar .tab { + flex: 0 0 auto; + padding: 0 24dp; + line-height: 64dp; + opacity: 0.35; + white-space: nowrap; + color: #E0DBC8; + decorator: vertical-gradient(#c2a42d00 #c2a42d00); + transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; +} + +.popup .tab-bar .tab.selected { + opacity: 1; + border-bottom: 4dp #C2A42D; + font-effect: glow(0dp 4dp 0dp 4dp black); +} + +.popup .tab-bar .tab:focus-visible, +.popup .tab-bar .tab:hover { + opacity: 1; + font-effect: glow(0dp 4dp 0dp 4dp black); + decorator: vertical-gradient(#c2a42d00 #c2a42d26); +} + +.popup .tab-bar .tab:active { + decorator: vertical-gradient(#c2a42d10 #c2a42d40); +} diff --git a/res/rml/popup.rml b/res/rml/popup.rml new file mode 100644 index 0000000000..967c5d9fb3 --- /dev/null +++ b/res/rml/popup.rml @@ -0,0 +1,11 @@ + + + Popup + + + + + + diff --git a/src/dusk/ui/nav_types.hpp b/src/dusk/ui/nav_types.hpp new file mode 100644 index 0000000000..7e485b9e8f --- /dev/null +++ b/src/dusk/ui/nav_types.hpp @@ -0,0 +1,17 @@ +#pragma once + +namespace dusk::ui { + +enum class NavCommand { + None, + Up, + Down, + Left, + Right, + Next, // R1 + Previous, // L1 + Confirm, // A + Cancel, // B +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/popup.cpp b/src/dusk/ui/popup.cpp new file mode 100644 index 0000000000..103ef79485 --- /dev/null +++ b/src/dusk/ui/popup.cpp @@ -0,0 +1,191 @@ +#include "popup.hpp" + +#include + +#include "aurora/rmlui.hpp" +#include "tab_button.hpp" +#include "ui.hpp" +#include "window.hpp" + +#include +#include +#include +#include + +namespace dusk::ui { + +Popup::Popup(Window& settingsWindow, Window& editorWindow) + : mSettingsWindow(settingsWindow), mEditorWindow(editorWindow) { + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return; + } + + mDocument = context->LoadDocument("res/rml/popup.rml"); + if (mDocument == nullptr) { + return; + } + + auto* tabBar = mDocument->GetElementById("tab-bar"); + if (tabBar == nullptr) { + return; + } + + const std::array tabLabels = { + "Settings", + "Warp", + "Editor", + "Reset", + "Exit", + }; + + // TODO: Make warp, reset, and exit buttons work + mTabActions = { + [this] { + hide(); + mSettingsWindow.show(); + mSettingsWindow.focus_for_input(); + set_selected_tab(0); + }, + [this] { + set_selected_tab(1); + }, + [this] { + hide(); + mEditorWindow.show(); + mEditorWindow.focus_for_input(); + set_selected_tab(2); + }, + [this] { + set_selected_tab(3); + }, + [this] { + set_selected_tab(4); + }, + }; + + mTabs.reserve(tabLabels.size()); + for (int i = 0; i < static_cast(tabLabels.size()); ++i) { + mTabs.push_back(create_tab_button(tabBar, tabLabels[i], i == mSelectedTabIndex, + [this, i](Rml::Event&) { + if (i >= 0 && i < static_cast(mTabActions.size())) { + mTabActions[i](); + } + })); + } + + mKeyListener = std::make_unique( + mDocument, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto cmd = map_nav_event(event); + if (cmd == NavCommand::None) { + return; + } + if (cmd == NavCommand::Left || cmd == NavCommand::Previous) { + focus_tab(std::max(0, mSelectedTabIndex - 1)); + event.StopPropagation(); + return; + } + if (cmd == NavCommand::Right || cmd == NavCommand::Next) { + focus_tab(std::min(static_cast(mTabs.size()) - 1, mSelectedTabIndex + 1)); + event.StopPropagation(); + return; + } + if (cmd == NavCommand::Confirm && mSelectedTabIndex >= 0 && + mSelectedTabIndex < static_cast(mTabActions.size())) + { + mTabActions[mSelectedTabIndex](); + event.StopPropagation(); + return; + } + if (cmd == NavCommand::Cancel) { + hide(); + event.StopPropagation(); + } + }); +} + +Popup::~Popup() { + auto* context = aurora::rmlui::get_context(); + if (context != nullptr && mDocument != nullptr) { + context->UnloadDocument(mDocument); + } +} + +void Popup::show() { + if (mDocument == nullptr) { + return; + } + + mHideDeadline.reset(); + mDocument->Show(); + mVisible = true; +} + +void Popup::hide() { + if (mDocument == nullptr) { + mVisible = false; + return; + } + + if (auto* popup = mDocument->GetElementById("popup")) { + popup->SetClass("popup-hidden", true); + mHideDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); // Must match the transition duration in popup.rcss + } else { + mDocument->Hide(); + } + mVisible = false; +} + +void Popup::toggle() { + if (is_visible()) { + hide(); + } else { + show(); + } +} + +bool Popup::is_visible() const { + return mVisible; +} + +void Popup::update() noexcept { + if (mDocument == nullptr) { + return; + } + if (mHideDeadline.has_value() && std::chrono::steady_clock::now() >= *mHideDeadline) { + mDocument->Hide(); + mHideDeadline.reset(); + } + if (mTabs.empty()) { + return; + } + std::vector tabs; + tabs.reserve(mTabs.size()); + for (const auto& tab : mTabs) { + tabs.push_back(tab.get()); + } + dusk::ui::set_selected_tab(tabs, mSelectedTabIndex); +} + +void Popup::set_selected_tab(int index) { + if (index < 0 || index >= static_cast(mTabs.size())) { + return; + } + mSelectedTabIndex = index; + std::vector tabs; + tabs.reserve(mTabs.size()); + for (const auto& tab : mTabs) { + tabs.push_back(tab.get()); + } + dusk::ui::set_selected_tab(tabs, mSelectedTabIndex); +} + +bool Popup::focus_tab(int index) { + if (index < 0 || index >= static_cast(mTabs.size())) { + return false; + } + set_selected_tab(index); + return mTabs[index]->focus(); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/popup.hpp b/src/dusk/ui/popup.hpp new file mode 100644 index 0000000000..002fffd0ca --- /dev/null +++ b/src/dusk/ui/popup.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include "button.hpp" +#include "event.hpp" + +#include +#include +#include +#include + +namespace dusk::ui { + +class Window; + +class Popup { +public: + Popup(Window& settingsWindow, Window& editorWindow); + ~Popup(); + + Popup(const Popup&) = delete; + Popup& operator=(const Popup&) = delete; + + void show(); + void hide(); + void toggle(); + bool is_visible() const; + void update() noexcept; + +private: + void set_selected_tab(int index); + bool focus_tab(int index); + + Window& mSettingsWindow; + Window& mEditorWindow; + Rml::ElementDocument* mDocument = nullptr; + std::vector > mTabs; + std::vector > mTabActions; + std::unique_ptr