mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-10 18:46:45 -04:00
Initial attempt at the overlay document
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user