From 57061fba381419e5f7fe2cf1a1b3c8cef53dc661 Mon Sep 17 00:00:00 2001 From: Irastris Date: Thu, 30 Apr 2026 20:49:13 -0400 Subject: [PATCH] Initial attempt at the overlay document --- files.cmake | 2 + res/rml/overlay.rcss | 157 +++++++++++++++++++++++++++ res/rml/overlay.rml | 22 ++++ src/dusk/ui/overlay.cpp | 229 +++++++++++++++++++++++++++++++++++++++ src/dusk/ui/overlay.hpp | 86 +++++++++++++++ src/dusk/ui/settings.cpp | 91 +++++++++++++++- 6 files changed, 585 insertions(+), 2 deletions(-) create mode 100644 res/rml/overlay.rcss create mode 100644 res/rml/overlay.rml create mode 100644 src/dusk/ui/overlay.cpp create mode 100644 src/dusk/ui/overlay.hpp 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