Initial attempt at the overlay document

This commit is contained in:
Irastris
2026-04-30 20:49:13 -04:00
parent cee6a24309
commit 57061fba38
6 changed files with 585 additions and 2 deletions
+2
View File
@@ -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
+157
View File
@@ -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;
}
+22
View File
@@ -0,0 +1,22 @@
<rml>
<head>
<title>Overlay</title>
<link type="text/rcss" href="overlay.rcss" />
</head>
<body>
<div class="overlay-root">
<div class="overlay">
<div class="header">
<div id="title"></div>
<div id="carousel-container" class="carousel-container"></div>
</div>
<div id="description" class="description"></div>
<div class="divider"></div>
<div class="footer">
<div id="return"></div>
<div id="reset"></div>
</div>
</div>
</div>
</body>
</rml>
+229
View File
@@ -0,0 +1,229 @@
#include "overlay.hpp"
#include <fmt/format.h>
#include <dolphin/gx/GXAurora.h>
#include <dolphin/vi.h>
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include <string>
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<float>(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<SteppedCarousel>(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<Button>(returnParent,
Button::Props{
.text = "Return",
.onPressed = [this](Rml::Event&) { dismiss(); },
}, "footer-button"
);
returnButton.root()->SetClass("return", true);
}
if (auto* resetParent = mDocument->GetElementById("reset")) {
auto& resetButton =
add_component<Button>(resetParent,
Button::Props{
.text = "Reset to default",
.onPressed = [this](Rml::Event&) { reset_default(); },
}, "footer-button"
);
resetButton.root()->SetClass("reset", true);
}
}
void Overlay::update() {
for (const auto& component : mComponents) {
component->update();
}
Document::update();
}
bool Overlay::focus() {
for (const auto& component : mComponents) {
if (component->focus()) {
return true;
}
}
return false;
}
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel) {
dismiss();
return true;
}
return Document::handle_nav_command(event, cmd);
}
void Overlay::dismiss() {
if (mDismissed) {
return;
}
mDismissed = true;
if (mOpenerDocument != nullptr) {
mOpenerDocument->Show();
}
hide();
}
void Overlay::reset_default() {
set_value(mOption, mValueMin);
}
} // namespace dusk::ui
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include "button.hpp"
#include "component.hpp"
#include "document.hpp"
#include "ui.hpp"
#include <functional>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
namespace dusk::ui {
class SteppedCarousel : public Component {
public:
struct Props {
int min = 0;
int max = 0;
int step = 1;
std::function<int()> getValue;
std::function<void(int)> onChange;
std::function<Rml::String(int)> formatValue;
};
SteppedCarousel(Rml::Element* parent, Props props);
bool focus() override;
void update() override;
private:
bool handle_nav_command(NavCommand cmd);
void apply(int value);
Props mProps;
Rml::Element* mValueElem = nullptr;
};
enum class GraphicsOption {
InternalResolution,
ShadowResolution,
};
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
struct OverlayProps {
GraphicsOption option;
Rml::String title;
Rml::String helpText;
int valueMin = 0;
int valueMax = 0;
Rml::ElementDocument* openerDocument = nullptr;
};
class Overlay : public Document {
public:
explicit Overlay(OverlayProps props);
void update() override;
bool focus() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
template <typename T, typename... Args>
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
auto child = std::make_unique<T>(std::forward<Args>(args)...);
T& ref = *child;
mComponents.emplace_back(std::move(child));
return ref;
}
void dismiss();
void reset_default();
GraphicsOption mOption;
int mValueMin = 0;
int mValueMax = 0;
Rml::ElementDocument* mOpenerDocument = nullptr;
std::vector<std::unique_ptr<Component> > mComponents;
bool mDismissed = false;
};
} // namespace dusk::ui
+89 -2
View File
@@ -7,10 +7,10 @@
#include "dusk/config.hpp"
#include "dusk/livesplit.h"
#include "m_Do/m_Do_main.h"
#include "overlay.hpp"
#include "pane.hpp"
#include "ui.hpp"
#include <algorithm>
#include <climits>
namespace dusk::ui {
@@ -172,6 +172,68 @@ private:
Rml::String mSuffix;
};
class ConfigOverlaySelect : public ConfigSelect<int> {
public:
struct Props {
Rml::String key;
ConfigVar<int>* value;
GraphicsOption option;
Rml::String title;
int min = 0;
int max = 0;
Rml::String helpText;
Pane* rightPane = nullptr;
std::function<bool()> isDisabled;
};
ConfigOverlaySelect(Rml::Element* parent, Props props)
: ConfigSelect<int>(parent,
{
.key = std::move(props.key),
.value = props.value,
.onChange = {},
.isDisabled = std::move(props.isDisabled),
.helpText = std::move(props.helpText),
.rightPane = props.rightPane,
}),
mOption(props.option), mTitle(std::move(props.title)), mValueMin(props.min),
mValueMax(props.max) {}
protected:
Rml::String get_value() override {
return format_graphics_setting_value(mOption, mVar->getValue());
}
bool handle_nav_command(NavCommand cmd) override {
if (cmd == NavCommand::Confirm) {
open_overlay();
return true;
}
return false;
}
private:
void open_overlay() {
auto* openerDocument = mRoot != nullptr ? mRoot->GetOwnerDocument() : nullptr;
if (openerDocument != nullptr) {
openerDocument->Hide();
}
auto& overlay = push_document(std::make_unique<Overlay>(OverlayProps{
.option = mOption,
.title = mTitle,
.helpText = mHelpText,
.valueMin = mValueMin,
.valueMax = mValueMax,
.openerDocument = openerDocument,
}));
}
GraphicsOption mOption;
Rml::String mTitle;
int mValueMin = 0;
int mValueMax = 0;
};
SettingsWindow::SettingsWindow() {
add_tab("Audio", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
@@ -421,7 +483,32 @@ SettingsWindow::SettingsWindow() {
});
leftPane.add_section("Resolution");
// TODO: Internal Resolution
leftPane.add_child<ConfigOverlaySelect>(leftPane.root(),
ConfigOverlaySelect::Props{
.key = "Internal Resolution",
.value = &getSettings().game.internalResolutionScale,
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.min = 0,
.max = 12,
.helpText =
"Configure the resolution used for rendering the game. Higher values are more "
"demanding on your graphics hardware.",
.rightPane = &rightPane,
});
leftPane.add_child<ConfigOverlaySelect>(leftPane.root(),
ConfigOverlaySelect::Props{
.key = "Shadow Resolution",
.value = &getSettings().game.shadowResolutionMultiplier,
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.min = 1,
.max = 8,
.helpText =
"Configure the shadow-map resolution. Higher values improve shadow quality but "
"increase GPU and memory usage.",
.rightPane = &rightPane,
});
leftPane.add_section("Post-Processing");
// TODO: Bloom