diff --git a/files.cmake b/files.cmake
index 798805a7c4..d7f51cdea9 100644
--- a/files.cmake
+++ b/files.cmake
@@ -1475,6 +1475,8 @@ set(DUSK_FILES
src/dusk/ui/nav_types.hpp
src/dusk/ui/number_button.cpp
src/dusk/ui/number_button.hpp
+ src/dusk/ui/overlay.cpp
+ src/dusk/ui/overlay.hpp
src/dusk/ui/pane.cpp
src/dusk/ui/pane.hpp
src/dusk/ui/popup.cpp
diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss
new file mode 100644
index 0000000000..de26e7188d
--- /dev/null
+++ b/res/rml/overlay.rcss
@@ -0,0 +1,157 @@
+*, *:before, *:after {
+ box-sizing: border-box;
+}
+
+body {
+ overflow: visible;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ font-family: "Fira Sans Condensed";
+ font-size: 24dp;
+ color: #FFFFFF;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: stretch;
+}
+
+.overlay-root {
+ width: 100%;
+ min-height: 45%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: stretch;
+ decorator: linear-gradient(0deg, #151610F2 0%, #151610CC 60%, #00000000 100%);
+ padding: 48dp 0 40dp 0;
+}
+
+button {
+ cursor: pointer;
+ focus: auto;
+}
+
+.overlay {
+ width: 100%;
+ max-width: 1216dp;
+ margin-left: auto;
+ margin-right: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 24dp;
+ padding: 0 32dp;
+}
+
+@media (max-height: 800dp) {
+ .overlay-root {
+ min-height: 38%;
+ padding: 32dp 0 28dp 0;
+ }
+
+ .overlay {
+ gap: 16dp;
+ padding: 0 24dp;
+ }
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24dp;
+}
+
+.carousel-container {
+ flex: 1 1 auto;
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+}
+
+.description {
+ font-size: 18dp;
+ line-height: 22dp;
+ color: rgba(255, 255, 255, 50%);
+}
+
+.divider {
+ margin: 2dp 0;
+ border-top: 2dp rgba(217, 217, 217, 50%);
+}
+
+.footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24dp;
+}
+
+.footer > div {
+ flex: 1 1 0;
+ display: flex;
+}
+
+#return {
+ justify-content: flex-start;
+}
+
+#reset {
+ justify-content: flex-end;
+}
+
+.footer-button {
+ display: block;
+ width: 100%;
+ max-width: 220dp;
+ border: 0;
+ padding: 0;
+ background-color: transparent;
+ font-family: "Fira Sans Condensed";
+ font-weight: bold;
+ font-size: 20dp;
+ line-height: 24dp;
+ text-transform: uppercase;
+ color: #FFFFFF;
+ opacity: 1;
+}
+
+.footer-button.return {
+ text-align: left;
+}
+
+.footer-button.reset {
+ text-align: right;
+}
+
+.stepped-carousel {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16dp;
+ width: auto;
+ min-width: 246dp;
+ padding: 0;
+ background-color: transparent;
+ font-family: "Fira Sans Condensed";
+ font-weight: bold;
+}
+
+.stepped-carousel-value {
+ line-height: 29dp;
+ min-width: 166dp;
+ text-align: center;
+ white-space: nowrap;
+ opacity: 0.9;
+}
+
+.stepped-carousel-arrow {
+ width: 24dp;
+ height: 24dp;
+ min-width: 24dp;
+ padding: 0;
+ border: 0;
+ background-color: transparent;
+ opacity: 1;
+}
diff --git a/res/rml/overlay.rml b/res/rml/overlay.rml
new file mode 100644
index 0000000000..b9456289fc
--- /dev/null
+++ b/res/rml/overlay.rml
@@ -0,0 +1,22 @@
+
+
+ Overlay
+
+
+
+
+
+
diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp
new file mode 100644
index 0000000000..1998247337
--- /dev/null
+++ b/src/dusk/ui/overlay.cpp
@@ -0,0 +1,229 @@
+#include "overlay.hpp"
+
+#include
+#include
+#include
+
+#include "dusk/config.hpp"
+#include "dusk/settings.h"
+
+#include
+
+namespace dusk::ui {
+namespace {
+
+int get_value(GraphicsOption option) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ return getSettings().game.internalResolutionScale.getValue();
+ case GraphicsOption::ShadowResolution:
+ return getSettings().game.shadowResolutionMultiplier.getValue();
+ }
+ return 0;
+}
+
+void set_value(GraphicsOption option, int value) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ getSettings().game.internalResolutionScale.setValue(value);
+ VISetFrameBufferScale(static_cast(value));
+ break;
+ case GraphicsOption::ShadowResolution:
+ getSettings().game.shadowResolutionMultiplier.setValue(value);
+ break;
+ }
+ config::Save();
+}
+
+Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
+ auto* doc = parent->GetOwnerDocument();
+ auto root = doc->CreateElement("div");
+ root->SetClass("stepped-carousel", true);
+ root->SetAttribute("tabindex", "0");
+ return parent->AppendChild(std::move(root));
+}
+
+Rml::Element* create_stepped_carousel_arrow(Rml::Element* parent, const Rml::String& className,
+ const Rml::String& label) {
+ auto* doc = parent->GetOwnerDocument();
+ auto button = doc->CreateElement("button");
+ button->SetClass("stepped-carousel-arrow", true);
+ button->SetClass(className, true);
+ button->SetInnerRML(escape(label));
+ return parent->AppendChild(std::move(button));
+}
+
+} // namespace
+
+SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
+ : Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
+ Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "<");
+ mValueElem = append(mRoot, "div");
+ mValueElem->SetClass("stepped-carousel-value", true);
+ Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ">");
+
+ listen(prevElem, Rml::EventId::Click, [this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
+ listen(nextElem, Rml::EventId::Click, [this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
+ listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
+ const auto cmd = map_nav_event(event);
+ if (cmd != NavCommand::None && handle_nav_command(cmd)) {
+ event.StopPropagation();
+ }
+ });
+}
+
+bool SteppedCarousel::focus() {
+ return Component::focus();
+}
+
+void SteppedCarousel::update() {
+ if (mValueElem == nullptr) {
+ return;
+ }
+ const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
+ if (mProps.formatValue) {
+ mValueElem->SetInnerRML(mProps.formatValue(value));
+ } else {
+ mValueElem->SetInnerRML(std::to_string(value));
+ }
+}
+
+bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
+ if (cmd == NavCommand::Left) {
+ const int value = mProps.getValue ? mProps.getValue() : 0;
+ apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
+ return true;
+ }
+ if (cmd == NavCommand::Right) {
+ const int value = mProps.getValue ? mProps.getValue() : 0;
+ apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
+ return true;
+ }
+ return false;
+}
+
+void SteppedCarousel::apply(int value) {
+ const int nextValue = std::clamp(value, mProps.min, mProps.max);
+ const int currentValue = std::clamp(mProps.getValue ? mProps.getValue()
+ : 0, mProps.min, mProps.max);
+ if (nextValue == currentValue) {
+ return;
+ }
+ if (mProps.onChange) {
+ mProps.onChange(nextValue);
+ }
+}
+
+Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ if (value <= 0) {
+ return "Auto";
+ } else {
+ u32 width = 0;
+ u32 height = 0;
+ AuroraGetRenderSize(&width, &height);
+ return fmt::format("{}x ({}x{})", value, width, height);
+ }
+ case GraphicsOption::ShadowResolution:
+ return fmt::format("{}x", value);
+ }
+ return "";
+}
+
+Overlay::Overlay(OverlayProps props)
+ : Document("res/rml/overlay.rml"),
+ mOption(props.option),
+ mValueMin(props.valueMin),
+ mValueMax(props.valueMax),
+ mOpenerDocument(props.openerDocument) {
+ if (mDocument == nullptr) {
+ return;
+ }
+
+ if (auto* title = mDocument->GetElementById("title")) {
+ title->SetInnerRML(escape(props.title));
+ }
+ if (auto* description = mDocument->GetElementById("description")) {
+ description->SetInnerRML(escape(props.helpText));
+ }
+ if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
+ add_component(carouselParent,
+ SteppedCarousel::Props{
+ .min = mValueMin,
+ .max = mValueMax,
+ .step = 1,
+ .getValue = [this] {
+ return get_value(mOption);
+ },
+ .onChange = [this](int value) {
+ set_value(mOption, value);
+ },
+ .formatValue = [this](int value) {
+ return format_graphics_setting_value(mOption, value);
+ },
+ });
+ }
+
+ if (auto* returnParent = mDocument->GetElementById("return")) {
+ auto& returnButton =
+ add_component