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