Revamped prelaunch experiment w/ RmlUi

This commit is contained in:
Luke Street
2026-04-27 00:18:31 -06:00
parent 3e1e8f1244
commit 25e9064d09
28 changed files with 2260 additions and 3 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
![DuskLogo](res/logo-mascot.webp)
![DuskLogo](res/logo-mascot.png)
- ### **[Official Website](https://twilitrealm.dev)**
- ### **[Discord](https://discord.gg/QACynxeyna)**
+1 -1
+20
View File
@@ -1462,6 +1462,26 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiStateShare.cpp
src/dusk/imgui/ImGuiAchievements.hpp
src/dusk/imgui/ImGuiAchievements.cpp
src/dusk/ui/button.hpp
src/dusk/ui/button.cpp
src/dusk/ui/disc_state.hpp
src/dusk/ui/disc_state.cpp
src/dusk/ui/element.hpp
src/dusk/ui/element.cpp
src/dusk/ui/focus_border.hpp
src/dusk/ui/focus_border.cpp
src/dusk/ui/game_option.hpp
src/dusk/ui/game_option.cpp
src/dusk/ui/label.hpp
src/dusk/ui/label.cpp
src/dusk/ui/prelaunch_layout.hpp
src/dusk/ui/prelaunch_layout.cpp
src/dusk/ui/prelaunch_screen.hpp
src/dusk/ui/prelaunch_screen.cpp
src/dusk/ui/theme.hpp
src/dusk/ui/theme.cpp
src/dusk/ui/ui.hpp
src/dusk/ui/ui.cpp
src/dusk/achievements.cpp
src/dusk/iso_validate.cpp
src/dusk/livesplit.cpp
Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

+2 -1
View File
@@ -22,6 +22,7 @@
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "dusk/ui/prelaunch_screen.hpp"
#include "m_Do/m_Do_controller_pad.h"
#include "m_Do/m_Do_main.h"
#include "tracy/Tracy.hpp"
@@ -339,7 +340,7 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
if (!dusk::IsGameLaunched) {
if (!dusk::IsGameLaunched && !dusk::ui::prelaunch::is_active()) {
m_preLaunchWindow.draw();
}
+4
View File
@@ -101,6 +101,10 @@ ValidationError validate(const char* path) {
NodHandleWrapper disc;
const auto sdlStream = SDL_IOFromFile(path, "rb");
if (sdlStream == nullptr) {
return ValidationError::IOError;
}
const NodDiscStream nod_stream {
.user_data = sdlStream,
.read_at = StreamReadAt,
+160
View File
@@ -0,0 +1,160 @@
#include "button.hpp"
#include "element.hpp"
#include "focus_border.hpp"
#include "label.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
#include <utility>
namespace dusk::ui {
namespace {
theme::Color variant_color(ButtonVariant variant) {
switch (variant) {
case ButtonVariant::Primary:
return theme::Primary;
case ButtonVariant::Secondary:
return theme::Secondary;
case ButtonVariant::Quiet:
default:
return theme::Elevated;
}
}
} // namespace
Button::Button(Rml::Element* parent, std::string_view id, std::string_view text,
ButtonVariant variant, std::function<void()> pressedCallback)
: m_variant(variant), m_pressedCallback(std::move(pressedCallback)) {
using namespace theme;
m_element = append(parent, "button", id);
set_props(m_element, {
{"display", "flex"},
{"position", "relative"},
{"flex-direction", "row"},
{"align-items", "center"},
{"justify-content", "center"},
{"box-sizing", "border-box"},
{"width", "100%"},
{"height", "68dp"},
{"min-height", "68dp"},
{"max-height", "68dp"},
{"padding-left", "22dp"},
{"padding-right", "22dp"},
{"border-width", dp(BorderWidth)},
{"border-radius", dp(BorderRadiusMedium)},
{"cursor", "pointer"},
{"tab-index", "auto"},
{"nav-up", "auto"},
{"nav-down", "auto"},
{"nav-left", "auto"},
{"nav-right", "auto"},
{"opacity", "1"},
{"font-family", "Inter"},
{"color", rgba(Text)},
});
add_focus_border(m_element, BorderRadiusMedium);
m_label = append_text(m_element, "span", text);
apply_label_style(m_label, LabelStyle::Medium);
set_props(m_label, {
{"pointer-events", "none"},
{"text-align", "center"},
});
m_element->AddEventListener(Rml::EventId::Click, this);
m_element->AddEventListener(Rml::EventId::Focus, this);
m_element->AddEventListener(Rml::EventId::Blur, this);
m_element->AddEventListener(Rml::EventId::Mouseover, this);
m_element->AddEventListener(Rml::EventId::Mouseout, this);
apply_style();
}
Button::~Button() {
if (m_element == nullptr) {
return;
}
m_element->RemoveEventListener(Rml::EventId::Click, this);
m_element->RemoveEventListener(Rml::EventId::Focus, this);
m_element->RemoveEventListener(Rml::EventId::Blur, this);
m_element->RemoveEventListener(Rml::EventId::Mouseover, this);
m_element->RemoveEventListener(Rml::EventId::Mouseout, this);
m_element = nullptr;
}
void Button::ProcessEvent(Rml::Event& event) {
switch (event.GetId()) {
case Rml::EventId::Click:
if (m_pressedCallback) {
m_pressedCallback();
}
break;
case Rml::EventId::Focus:
m_focused = true;
apply_style();
break;
case Rml::EventId::Blur:
m_focused = false;
apply_style();
break;
case Rml::EventId::Mouseover:
m_focused = true;
apply_style();
break;
case Rml::EventId::Mouseout:
m_focused = false;
apply_style();
break;
default:
break;
}
}
Rml::Element* Button::element() const {
return m_element;
}
std::string Button::id() const {
return m_element == nullptr ? std::string{} : m_element->GetId();
}
void Button::set_text(std::string_view text) {
ui::set_text(m_label, text);
}
void Button::apply_style() {
using namespace theme;
if (m_element == nullptr) {
return;
}
const bool active = m_hovered || m_focused;
const bool isBasic = m_variant == ButtonVariant::Quiet;
const Color color = variant_color(m_variant);
int borderOpacity = isBasic ? 0 : 190;
int backgroundOpacity = isBasic ? 0 : 28;
int backgroundHoverOpacity = 116;
int borderHoverOpacity = isBasic ? backgroundHoverOpacity : 255;
if (m_variant == ButtonVariant::Quiet) {
backgroundHoverOpacity = 68;
borderHoverOpacity = 150;
}
m_element->SetProperty("border-color",
rgba(color, active ? borderHoverOpacity : borderOpacity));
m_element->SetProperty("background-color",
rgba(color, active ? backgroundHoverOpacity : backgroundOpacity));
m_element->SetProperty("color", rgba(active ? TextActive : Text));
m_label->SetProperty("color", rgba(active ? TextActive : Text));
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <RmlUi/Core/EventListener.h>
#include <functional>
#include <string>
#include <string_view>
namespace Rml {
class Element;
}
namespace dusk::ui {
enum class ButtonVariant {
Primary,
Secondary,
Quiet,
};
class Button : public Rml::EventListener {
public:
Button(Rml::Element* parent, std::string_view id, std::string_view text, ButtonVariant variant,
std::function<void()> pressedCallback);
~Button() override;
Button(const Button&) = delete;
Button& operator=(const Button&) = delete;
void ProcessEvent(Rml::Event& event) override;
Rml::Element* element() const;
std::string id() const;
void set_text(std::string_view text);
private:
Rml::Element* m_element = nullptr;
Rml::Element* m_label = nullptr;
ButtonVariant m_variant = ButtonVariant::Secondary;
std::function<void()> m_pressedCallback;
bool m_hovered = false;
bool m_focused = false;
void apply_style();
};
} // namespace dusk::ui
+133
View File
@@ -0,0 +1,133 @@
#include "disc_state.hpp"
#include "element.hpp"
#include "focus_border.hpp"
#include "label.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
#include <utility>
namespace dusk::ui {
DiscState::DiscState(Rml::Element* parent, std::string_view id, std::string_view text, bool error,
std::function<void()> pressedCallback)
: m_pressedCallback(std::move(pressedCallback)), m_error(error) {
using namespace theme;
m_element = append(parent, "button", id);
set_props(m_element, {
{"display", "flex"},
{"position", "relative"},
{"flex-direction", "column"},
{"align-items", "stretch"},
{"gap", "6dp"},
{"width", "100%"},
{"box-sizing", "border-box"},
{"padding", "14dp 16dp"},
{"border-width", dp(BorderWidth)},
{"border-radius", dp(BorderRadiusSmall)},
{"cursor", "pointer"},
{"tab-index", "auto"},
{"nav-up", "auto"},
{"nav-down", "auto"},
{"nav-left", "auto"},
{"nav-right", "auto"},
{"font-family", "Inter"},
});
add_focus_border(m_element, BorderRadiusSmall);
m_label = add_label(m_element, error ? "Disc Error" : "Selected Disc", LabelStyle::Annotation);
set_props(m_label, {
{"pointer-events", "none"},
});
m_value = add_label(m_element, text, LabelStyle::Body);
set_props(m_value, {
{"overflow", "hidden"},
{"text-overflow", "ellipsis"},
{"white-space", "nowrap"},
{"pointer-events", "none"},
});
m_element->AddEventListener(Rml::EventId::Click, this);
m_element->AddEventListener(Rml::EventId::Focus, this);
m_element->AddEventListener(Rml::EventId::Blur, this);
m_element->AddEventListener(Rml::EventId::Mouseover, this);
m_element->AddEventListener(Rml::EventId::Mouseout, this);
apply_style();
}
DiscState::~DiscState() {
if (m_element == nullptr) {
return;
}
m_element->RemoveEventListener(Rml::EventId::Click, this);
m_element->RemoveEventListener(Rml::EventId::Focus, this);
m_element->RemoveEventListener(Rml::EventId::Blur, this);
m_element->RemoveEventListener(Rml::EventId::Mouseover, this);
m_element->RemoveEventListener(Rml::EventId::Mouseout, this);
m_element = nullptr;
}
void DiscState::ProcessEvent(Rml::Event& event) {
switch (event.GetId()) {
case Rml::EventId::Click:
if (m_pressedCallback) {
m_pressedCallback();
}
break;
case Rml::EventId::Focus:
m_focused = true;
apply_style();
break;
case Rml::EventId::Blur:
m_focused = false;
apply_style();
break;
case Rml::EventId::Mouseover:
m_hovered = true;
apply_style();
break;
case Rml::EventId::Mouseout:
m_hovered = false;
apply_style();
break;
default:
break;
}
}
Rml::Element* DiscState::element() const {
return m_element;
}
std::string DiscState::id() const {
return m_element == nullptr ? std::string{} : m_element->GetId();
}
void DiscState::apply_style() {
using namespace theme;
if (m_element == nullptr) {
return;
}
const bool active = m_hovered || m_focused;
const Color accent = m_error ? Danger : Primary;
m_element->SetProperty("background-color", rgba(accent, active ? 52 : (m_error ? 32 : 20)));
m_element->SetProperty("border-color", rgba(accent, active ? 220 : (m_error ? 190 : 120)));
m_element->SetProperty("color", rgba(active ? TextActive : Text));
m_label->SetProperty("color", rgba(m_error ? Danger : (active ? TextActive : TextDim),
m_error ? 220 : (active ? TextActive.a : TextDim.a)));
m_value->SetProperty("color", rgba(m_error ? Danger : (active ? TextActive : Text),
m_error ? 255 : (active ? TextActive.a : Text.a)));
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <RmlUi/Core/EventListener.h>
#include <functional>
#include <string>
#include <string_view>
namespace Rml {
class Element;
}
namespace dusk::ui {
class DiscState : public Rml::EventListener {
public:
DiscState(Rml::Element* parent, std::string_view id, std::string_view text, bool error,
std::function<void()> pressedCallback);
~DiscState() override;
DiscState(const DiscState&) = delete;
DiscState& operator=(const DiscState&) = delete;
void ProcessEvent(Rml::Event& event) override;
Rml::Element* element() const;
std::string id() const;
private:
Rml::Element* m_element = nullptr;
Rml::Element* m_label = nullptr;
Rml::Element* m_value = nullptr;
std::function<void()> m_pressedCallback;
bool m_error = false;
bool m_hovered = false;
bool m_focused = false;
void apply_style();
};
} // namespace dusk::ui
+81
View File
@@ -0,0 +1,81 @@
#include "element.hpp"
#include <RmlUi/Core/ElementDocument.h>
#include <string>
namespace dusk::ui {
std::string escape(std::string_view text) {
std::string result;
result.reserve(text.size());
for (char c : text) {
switch (c) {
case '&':
result += "&amp;";
break;
case '<':
result += "&lt;";
break;
case '>':
result += "&gt;";
break;
case '"':
result += "&quot;";
break;
case '\'':
result += "&apos;";
break;
default:
result += c;
break;
}
}
return result;
}
Rml::Element* append(Rml::Element* parent, std::string_view tag, std::string_view id) {
if (parent == nullptr) {
return nullptr;
}
Rml::ElementDocument* document = parent->GetOwnerDocument();
if (document == nullptr) {
document = dynamic_cast<Rml::ElementDocument*>(parent);
}
if (document == nullptr) {
return nullptr;
}
Rml::ElementPtr child = document->CreateElement(std::string(tag));
Rml::Element* rawChild = child.get();
if (!id.empty()) {
rawChild->SetId(std::string(id));
}
return parent->AppendChild(std::move(child));
}
Rml::Element* append_text(Rml::Element* parent, std::string_view tag, std::string_view text,
std::string_view id) {
Rml::Element* element = append(parent, tag, id);
set_text(element, text);
return element;
}
void set_text(Rml::Element* element, std::string_view text) {
if (element != nullptr) {
element->SetInnerRML(escape(text));
}
}
void set_props(Rml::Element* element,
std::initializer_list<std::pair<std::string_view, std::string_view> > properties) {
if (element == nullptr) {
return;
}
for (const auto& [name, value] : properties) {
element->SetProperty(std::string(name), std::string(value));
}
}
} // namespace dusk::ui
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <RmlUi/Core/Element.h>
#include <initializer_list>
#include <string_view>
namespace dusk::ui {
std::string escape(std::string_view text);
Rml::Element* append(Rml::Element* parent, std::string_view tag, std::string_view id = {});
Rml::Element* append_text(Rml::Element* parent, std::string_view tag, std::string_view text,
std::string_view id = {});
void set_text(Rml::Element* element, std::string_view text);
void set_props(Rml::Element* element,
std::initializer_list<std::pair<std::string_view, std::string_view> > properties);
} // namespace dusk::ui
+36
View File
@@ -0,0 +1,36 @@
#include "focus_border.hpp"
#include "element.hpp"
#include "theme.hpp"
namespace dusk::ui {
Rml::Element* add_focus_border(Rml::Element* parent, float radius) {
using namespace theme;
auto* border = append(parent, "div");
set_props(border, {
{"position", "absolute"},
{"pointer-events", "none"},
{"left", dp(-(BorderWidth * 3.0f))},
{"top", dp(-(BorderWidth * 3.0f))},
{"right", dp(-(BorderWidth * 3.0f))},
{"bottom", dp(-(BorderWidth * 3.0f))},
{"border-width", dp(BorderWidth * 2.0f)},
{"border-radius", dp(radius + BorderWidth * 4.0f)},
{"border-color", rgba(PrimaryLight, 0)},
});
return border;
}
void set_focus_border_visible(Rml::Element* parent, bool visible) {
if (parent == nullptr || parent->GetNumChildren() == 0) {
return;
}
parent->GetChild(0)->SetProperty("border-color", visible ?
theme::rgba(theme::PrimaryLight, 255) :
theme::rgba(theme::PrimaryLight, 0));
}
} // namespace dusk::ui
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <RmlUi/Core/Element.h>
namespace dusk::ui {
Rml::Element* add_focus_border(Rml::Element* parent, float radius);
void set_focus_border_visible(Rml::Element* parent, bool visible);
} // namespace dusk::ui
+187
View File
@@ -0,0 +1,187 @@
#include "game_option.hpp"
#include "element.hpp"
#include "focus_border.hpp"
#include "label.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
#include <utility>
namespace dusk::ui {
GameOption::GameOption(Rml::Element* parent, std::string_view id, std::string_view title,
std::string_view value, std::string_view detail,
std::function<void()> pressedCallback)
: m_pressedCallback(std::move(pressedCallback)) {
using namespace theme;
m_element = append(parent, "button", id);
set_props(m_element, {
{"display", "flex"},
{"position", "relative"},
{"flex-direction", "row"},
{"align-items", "center"},
{"justify-content", "space-between"},
{"box-sizing", "border-box"},
{"gap", "16dp"},
{"width", "100%"},
{"height", "auto"},
{"padding", "16dp"},
{"border-width", dp(BorderWidth)},
{"border-radius", dp(BorderRadiusSmall)},
{"background-color", rgba(Transparent)},
{"border-color", rgba(ElevatedBorder, 0)},
{"color", rgba(TextDim)},
{"cursor", "pointer"},
{"tab-index", "auto"},
{"nav-up", "auto"},
{"nav-down", "auto"},
{"nav-left", "auto"},
{"nav-right", "auto"},
{"opacity", "1"},
{"font-family", "Inter"},
});
add_focus_border(m_element, BorderRadiusSmall);
auto* left = append(m_element, "div");
set_props(left, {
{"display", "flex"},
{"flex-direction", "column"},
{"gap", "4dp"},
{"min-width", "0"},
{"width", "0"},
{"flex-grow", "1"},
{"flex-shrink", "1"},
{"pointer-events", "none"},
});
m_title = add_label(left, title, LabelStyle::Large);
set_props(m_title, {
{"color", rgba(TextDim)},
{"font-size", "28dp"},
{"letter-spacing", "1dp"},
});
if (!value.empty() || !detail.empty()) {
auto* right = append(m_element, "div");
set_props(right, {
{"display", "flex"},
{"flex-direction", "column"},
{"align-items", "flex-end"},
{"justify-content", "center"},
{"gap", "4dp"},
{"min-width", "170dp"},
{"max-width", "48%"},
{"flex-shrink", "0"},
{"pointer-events", "none"},
});
if (!value.empty()) {
m_value = add_label(right, value, LabelStyle::Body);
set_props(m_value, {
{"color", rgba(TextDim)},
{"text-align", "right"},
{"overflow", "hidden"},
{"text-overflow", "ellipsis"},
{"white-space", "nowrap"},
});
}
if (!detail.empty()) {
m_detail = add_label(right, detail, LabelStyle::Annotation);
set_props(m_detail, {
{"color", rgba(TextDim)},
{"text-align", "right"},
});
}
}
m_element->AddEventListener(Rml::EventId::Click, this);
m_element->AddEventListener(Rml::EventId::Focus, this);
m_element->AddEventListener(Rml::EventId::Blur, this);
m_element->AddEventListener(Rml::EventId::Mouseover, this);
m_element->AddEventListener(Rml::EventId::Mouseout, this);
apply_style();
}
GameOption::~GameOption() {
if (m_element == nullptr) {
return;
}
m_element->RemoveEventListener(Rml::EventId::Click, this);
m_element->RemoveEventListener(Rml::EventId::Focus, this);
m_element->RemoveEventListener(Rml::EventId::Blur, this);
m_element->RemoveEventListener(Rml::EventId::Mouseover, this);
m_element->RemoveEventListener(Rml::EventId::Mouseout, this);
m_element = nullptr;
}
void GameOption::ProcessEvent(Rml::Event& event) {
switch (event.GetId()) {
case Rml::EventId::Click:
if (m_pressedCallback) {
m_pressedCallback();
}
break;
case Rml::EventId::Focus:
m_focused = true;
apply_style();
break;
case Rml::EventId::Blur:
m_focused = false;
apply_style();
break;
case Rml::EventId::Mouseover:
m_hovered = true;
apply_style();
break;
case Rml::EventId::Mouseout:
m_hovered = false;
apply_style();
break;
default:
break;
}
}
Rml::Element* GameOption::element() const {
return m_element;
}
std::string GameOption::id() const {
return m_element == nullptr ? std::string{} : m_element->GetId();
}
void GameOption::set_value(std::string_view value) {
set_text(m_value, value);
}
void GameOption::apply_style() {
if (m_element == nullptr) {
return;
}
const bool active = m_hovered || m_focused;
m_element->SetProperty("background-color", active ? theme::rgba(theme::Primary, 52) :
theme::rgba(theme::Primary, 0));
m_element->SetProperty("border-color", active ? theme::rgba(theme::Primary, 220) :
theme::rgba(theme::ElevatedBorder, 42));
m_element->SetProperty("color",
active ? theme::rgba(theme::TextActive) : theme::rgba(theme::TextDim));
m_title->SetProperty("color",
active ? theme::rgba(theme::TextActive) : theme::rgba(theme::TextDim));
if (m_value != nullptr) {
m_value->SetProperty("color",
active ? theme::rgba(theme::TextActive) : theme::rgba(theme::TextDim));
}
if (m_detail != nullptr) {
m_detail->SetProperty("color", theme::rgba(theme::TextDim));
}
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <RmlUi/Core/EventListener.h>
#include <functional>
#include <string>
#include <string_view>
namespace Rml {
class Element;
}
namespace dusk::ui {
class GameOption : public Rml::EventListener {
public:
GameOption(Rml::Element* parent, std::string_view id, std::string_view title,
std::string_view value, std::string_view detail,
std::function<void()> pressedCallback);
~GameOption() override;
GameOption(const GameOption&) = delete;
GameOption& operator=(const GameOption&) = delete;
void ProcessEvent(Rml::Event& event) override;
Rml::Element* element() const;
std::string id() const;
void set_value(std::string_view value);
private:
Rml::Element* m_element = nullptr;
Rml::Element* m_title = nullptr;
Rml::Element* m_value = nullptr;
Rml::Element* m_detail = nullptr;
std::function<void()> m_pressedCallback;
bool m_hovered = false;
bool m_focused = false;
void apply_style();
};
} // namespace dusk::ui
+45
View File
@@ -0,0 +1,45 @@
#include "label.hpp"
#include "element.hpp"
#include "theme.hpp"
namespace dusk::ui {
void apply_label_style(Rml::Element* element, LabelStyle style) {
using namespace theme;
switch (style) {
case LabelStyle::Annotation:
set_props(element, {{"font-size", "18dp"},
{"letter-spacing", "2dp"},
{"font-weight", "400"},
{"color", rgba(TextDim)}});
break;
case LabelStyle::Body:
set_props(element, {{"font-size", "20dp"},
{"letter-spacing", "0"},
{"font-weight", "400"},
{"color", rgba(Text)}});
break;
case LabelStyle::Medium:
set_props(element, {{"font-size", "28dp"},
{"letter-spacing", "3dp"},
{"font-weight", "700"},
{"color", rgba(Text)}});
break;
case LabelStyle::Large:
set_props(element, {{"font-size", "36dp"},
{"letter-spacing", "4dp"},
{"font-weight", "700"},
{"color", rgba(Text)}});
break;
}
}
Rml::Element* add_label(Rml::Element* parent, std::string_view text, LabelStyle style) {
Rml::Element* label = append_text(parent, "div", text);
apply_label_style(label, style);
return label;
}
} // namespace dusk::ui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <RmlUi/Core/Element.h>
#include <string_view>
namespace dusk::ui {
enum class LabelStyle {
Annotation,
Body,
Medium,
Large,
};
Rml::Element* add_label(Rml::Element* parent, std::string_view text, LabelStyle style);
void apply_label_style(Rml::Element* element, LabelStyle style);
} // namespace dusk::ui
+113
View File
@@ -0,0 +1,113 @@
#include "prelaunch_layout.hpp"
#include "element.hpp"
#include "label.hpp"
#include "theme.hpp"
namespace dusk::ui::prelaunch::layout {
void style_document(Rml::ElementDocument* document) {
using namespace theme;
set_props(document, {
{"width", "100%"},
{"height", "100%"},
{"margin", "0"},
{"padding", "0"},
{"font-family", "Inter"},
{"background-color", rgba(Background1)},
{"color", rgba(Text)},
});
}
Rml::Element* add_screen(Rml::ElementDocument* document, ScreenLayout layout) {
using namespace theme;
auto* screen = append(document, "div", "prelaunch-screen");
set_props(screen,
{
{"display", "flex"},
{"position", "absolute"},
{"left", "0"},
{"top", "0"},
{"right", "0"},
{"bottom", "0"},
{"flex-direction", layout == ScreenLayout::CompactSplit ? "row" : "column"},
{"align-items", "center"},
{"justify-content", "center"},
{"gap", layout == ScreenLayout::CompactSplit ? "28dp" : "24dp"},
{"box-sizing", "border-box"},
{"padding", layout == ScreenLayout::CompactSplit ? "24dp" : "48dp 28dp"},
{"background-color", rgba(Background1)},
});
return screen;
}
Rml::Element* add_brand(Rml::Element* parent, std::string_view logoPath, bool compact) {
auto* brand = append(parent, "div");
set_props(brand, {
{"display", "flex"},
{"flex-direction", "column"},
{"align-items", "center"},
{"justify-content", "center"},
{"gap", compact ? "8dp" : "12dp"},
{"width", compact ? "260dp" : "100%"},
{"max-width", compact ? "32%" : "720dp"},
{"flex-shrink", compact ? "0" : "1"},
});
auto* subtitle = add_label(brand, "Twilit Realm presents", LabelStyle::Annotation);
set_props(subtitle, {
{"text-align", "center"},
{"font-size", compact ? "14dp" : "18dp"},
});
if (!logoPath.empty()) {
auto* logo = append(brand, "img");
logo->SetAttribute("src", std::string(logoPath));
set_props(logo, {
{"width", compact ? "220dp" : "360dp"},
{"max-width", compact ? "100%" : "70%"},
{"height", "auto"},
});
} else {
auto* title = add_label(brand, "Dusk", LabelStyle::Large);
set_props(title, {
{"font-size", compact ? "42dp" : "54dp"},
{"letter-spacing", compact ? "3dp" : "4dp"},
});
}
return brand;
}
Rml::Element* add_heading(Rml::Element* parent, std::string_view title) {
auto* heading = add_label(parent, title, LabelStyle::Large);
set_props(heading, {
{"width", "100%"},
{"max-width", "840dp"},
{"text-align", "left"},
});
return heading;
}
Rml::Element* add_panel(Rml::Element* parent, bool wide, bool compact) {
using namespace theme;
auto* panel = append(parent, "div");
set_props(panel, {
{"display", "flex"},
{"flex-direction", "column"},
{"gap", "12dp"},
{"width", wide ? "840dp" : "520dp"},
{"max-width", compact ? "62%" : "100%"},
{"box-sizing", "border-box"},
{"padding", compact ? "16dp" : "20dp"},
{"border-width", dp(BorderWidth)},
{"border-radius", dp(BorderRadiusMedium)},
{"border-color", rgba(ElevatedBorder, ElevatedBorder.a)},
{"background-color", rgba(ElevatedSoft, ElevatedSoft.a)},
});
return panel;
}
} // namespace dusk::ui::prelaunch::layout
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <RmlUi/Core/Element.h>
#include <RmlUi/Core/ElementDocument.h>
#include <string_view>
namespace dusk::ui::prelaunch::layout {
enum class ScreenLayout {
Standard,
CompactSplit,
};
void style_document(Rml::ElementDocument* document);
Rml::Element* add_screen(Rml::ElementDocument* document,
ScreenLayout layout = ScreenLayout::Standard);
Rml::Element* add_brand(Rml::Element* parent, std::string_view logoPath, bool compact = false);
Rml::Element* add_heading(Rml::Element* parent, std::string_view title);
Rml::Element* add_panel(Rml::Element* parent, bool wide, bool compact = false);
} // namespace dusk::ui::prelaunch::layout
+924
View File
@@ -0,0 +1,924 @@
#include "prelaunch_screen.hpp"
#include "button.hpp"
#include "disc_state.hpp"
#include "game_option.hpp"
#include "prelaunch_layout.hpp"
#include "ui.hpp"
#include "../file_select.hpp"
#include "../iso_validate.hpp"
#include "dusk/config.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include <RmlUi/Core.h>
#include <RmlUi/Core/ElementDocument.h>
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_filesystem.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_misc.h>
#include <aurora/aurora.h>
#include <dolphin/card.h>
#include <fmt/format.h>
#include "aurora/lib/window.hpp"
#include <aurora/rmlui.hpp>
#include <array>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace dusk::ui::prelaunch {
namespace {
enum class View {
Main,
Options,
LanguageSelect,
GraphicsBackendSelect,
SaveFileTypeSelect,
};
struct BackendChoice {
AuroraBackend backend = BACKEND_AUTO;
std::string id;
std::string name;
};
constexpr std::array<const char*, 5> kLanguageNames = {
"English", "German", "French", "Spanish", "Italian",
};
constexpr std::array<SDL_DialogFileFilter, 2> kGameDiscFileFilters{{
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
{"All Files", "*"},
}};
std::string iso_validation_error_message(iso::ValidationError code) {
switch (code) {
case iso::ValidationError::IOError:
return "Unable to read selected disc image";
case iso::ValidationError::InvalidImage:
return "Unable to interpret selected file as a disc image";
case iso::ValidationError::WrongGame:
return "Selected disc is for a different game";
case iso::ValidationError::WrongVersion:
return "Selected disc is for an unsupported version. Only NTSC & PAL GameCube are "
"supported at this time";
case iso::ValidationError::ExecutableMismatch:
return "Selected disc image contains modified executable files.";
case iso::ValidationError::Success:
return {};
case iso::ValidationError::Unknown:
default:
return "Unknown disc image validation error";
}
}
std::string_view backend_name(AuroraBackend backend) {
switch (backend) {
default:
return "Auto";
case BACKEND_D3D12:
return "D3D12";
case BACKEND_D3D11:
return "D3D11";
case BACKEND_METAL:
return "Metal";
case BACKEND_VULKAN:
return "Vulkan";
case BACKEND_OPENGL:
return "OpenGL";
case BACKEND_OPENGLES:
return "OpenGL ES";
case BACKEND_WEBGPU:
return "WebGPU";
case BACKEND_NULL:
return "Null";
}
}
std::string_view backend_id(AuroraBackend backend) {
switch (backend) {
default:
return "auto";
case BACKEND_D3D12:
return "d3d12";
case BACKEND_D3D11:
return "d3d11";
case BACKEND_METAL:
return "metal";
case BACKEND_VULKAN:
return "vulkan";
case BACKEND_OPENGL:
return "opengl";
case BACKEND_OPENGLES:
return "opengles";
case BACKEND_WEBGPU:
return "webgpu";
case BACKEND_NULL:
return "null";
}
}
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
return true;
}
if (backend == "d3d11") {
outBackend = BACKEND_D3D11;
return true;
}
if (backend == "d3d12") {
outBackend = BACKEND_D3D12;
return true;
}
if (backend == "metal") {
outBackend = BACKEND_METAL;
return true;
}
if (backend == "vulkan") {
outBackend = BACKEND_VULKAN;
return true;
}
if (backend == "opengl") {
outBackend = BACKEND_OPENGL;
return true;
}
if (backend == "opengles") {
outBackend = BACKEND_OPENGLES;
return true;
}
if (backend == "webgpu") {
outBackend = BACKEND_WEBGPU;
return true;
}
if (backend == "null") {
outBackend = BACKEND_NULL;
return true;
}
return false;
}
std::string_view card_type_name(CARDFileType type) {
switch (type) {
case CARD_GCIFOLDER:
return "GCI Folder";
case CARD_RAWIMAGE:
return "Card Image";
default:
return "";
}
}
std::filesystem::path resource_path(const char* filename) {
const char* basePath = SDL_GetBasePath();
if (basePath == nullptr) {
return std::filesystem::path("res") / filename;
}
return std::filesystem::path(basePath) / "res" / filename;
}
std::string display_path(std::string_view path) {
const char* home = SDL_GetUserFolder(SDL_FOLDER_HOME);
if (home == nullptr || home[0] == '\0') {
home = std::getenv("HOME");
}
if (home == nullptr || home[0] == '\0') {
return std::string(path);
}
std::string homePath(home);
while (homePath.size() > 1 && homePath.back() == '/') {
homePath.pop_back();
}
if (path == homePath) {
return "~";
}
if (path.size() > homePath.size() && path.substr(0, homePath.size()) == homePath &&
path[homePath.size()] == '/')
{
return "~" + std::string(path.substr(homePath.size()));
}
return std::string(path);
}
std::vector<BackendChoice> backend_choices() {
std::vector<BackendChoice> choices;
choices.push_back({BACKEND_AUTO, std::string(backend_id(BACKEND_AUTO)),
std::string(backend_name(BACKEND_AUTO))});
size_t backendCount = 0;
const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount);
for (size_t i = 0; i < backendCount; ++i) {
const AuroraBackend backend = availableBackends[i];
choices.push_back(
{backend, std::string(backend_id(backend)), std::string(backend_name(backend))});
}
return choices;
}
class Screen : public Rml::EventListener {
public:
bool initialize() {
if (m_initialized) {
return true;
}
if (!ui::initialize()) {
return false;
}
m_selectedIsoPath = getSettings().backend.isoPath.getValue();
validate_selected_iso(false);
m_initialGraphicsBackend = getSettings().backend.graphicsBackend.getValue();
m_initialized = true;
if (is_selected_path_valid() && getSettings().backend.skipPreLaunchUI.getValue()) {
IsGameLaunched = true;
return true;
}
set_active(true);
rebuild();
if (m_document == nullptr) {
shutdown();
return false;
}
return true;
}
void shutdown() {
close_document();
set_active(false);
m_initialized = false;
m_focusIds.clear();
}
bool is_active() const { return m_initialized && ui::is_active(); }
void handle_event(const SDL_Event& event) { ui::handle_event(event); }
void update() {
if (m_requestedBack) {
m_requestedBack = false;
back();
if (!is_active()) {
return;
}
}
if (!m_requestedActivation.empty()) {
const std::string requestedActivation = m_requestedActivation;
m_requestedActivation.clear();
activate(requestedActivation);
if (!is_active()) {
return;
}
}
if (m_requestedCycleDirection != 0) {
const std::string requestedCycleId = m_requestedCycleId;
const int requestedCycleDirection = m_requestedCycleDirection;
m_requestedCycleId.clear();
m_requestedCycleDirection = 0;
cycle_option(requestedCycleId, requestedCycleDirection);
if (!is_active()) {
return;
}
}
if (is_selected_path_valid() && getSettings().backend.skipPreLaunchUI.getValue()) {
IsGameLaunched = true;
shutdown();
return;
}
ui::update();
rebuild_if_layout_changed();
}
void ProcessEvent(Rml::Event& event) override {
if (event.GetId() == Rml::EventId::Keydown) {
const auto key = static_cast<Rml::Input::KeyIdentifier>(
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
if (handle_key(key)) {
event.StopImmediatePropagation();
}
}
}
private:
bool m_initialized = false;
View m_view = View::Main;
Rml::ElementDocument* m_document = nullptr;
std::vector<std::string> m_focusIds;
std::vector<std::unique_ptr<Button> > m_buttons;
std::unique_ptr<DiscState> m_discState;
std::vector<std::unique_ptr<GameOption> > m_options;
std::string m_pendingFocusId;
std::string m_requestedActivation;
std::string m_requestedCycleId;
int m_requestedCycleDirection = 0;
bool m_requestedBack = false;
std::string m_selectedIsoPath;
std::string m_errorString;
std::string m_initialGraphicsBackend;
bool m_compactLayout = false;
bool m_selectedIsoValid = false;
bool m_isPal = false;
bool selected_path_exists() const {
#if TARGET_ANDROID
return !m_selectedIsoPath.empty();
#else
return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr);
#endif
}
bool is_selected_path_valid() const { return m_selectedIsoValid; }
void validate_selected_iso(bool save_valid_path) {
m_errorString.clear();
m_selectedIsoValid = false;
m_isPal = false;
if (m_selectedIsoPath.empty()) {
return;
}
if (!selected_path_exists()) {
m_errorString = "Selected disc image does not exist";
return;
}
const iso::ValidationError validationResult = iso::validate(m_selectedIsoPath.c_str());
if (validationResult != iso::ValidationError::Success) {
m_errorString = iso_validation_error_message(validationResult);
return;
}
m_selectedIsoValid = true;
m_isPal = iso::isPal(m_selectedIsoPath.c_str());
if (save_valid_path) {
getSettings().backend.isoPath.setValue(m_selectedIsoPath);
Save();
}
}
void close_document() {
if (m_document == nullptr) {
return;
}
m_document->RemoveEventListener(Rml::EventId::Keydown, this);
m_buttons.clear();
m_discState.reset();
m_options.clear();
m_focusIds.clear();
m_document->Close();
m_document = nullptr;
}
std::string logo_path() const {
const auto logo_path = resource_path("logo-mascot.png");
if (std::filesystem::exists(logo_path)) {
return logo_path.string();
}
return {};
}
std::string disc_state_text() const {
if (!m_errorString.empty()) {
return m_errorString;
}
if (is_selected_path_valid()) {
return display_path(m_selectedIsoPath);
}
return "No disc image selected";
}
bool should_use_compact_layout() const {
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return false;
}
const Rml::Vector2i dimensions = context->GetDimensions();
float dp_ratio = context->GetDensityIndependentPixelRatio();
if (dp_ratio <= 0.0f) {
dp_ratio = 1.0f;
}
const float width = static_cast<float>(dimensions.x) / dp_ratio;
const float height = static_cast<float>(dimensions.y) / dp_ratio;
return height < 680.0f && width >= 720.0f;
}
void rebuild_if_layout_changed() {
const bool compactLayout = should_use_compact_layout();
if (compactLayout == m_compactLayout) {
return;
}
if (Rml::Context* context = aurora::rmlui::get_context()) {
if (Rml::Element* focused = context->GetFocusElement()) {
m_pendingFocusId = focused->GetId();
}
}
rebuild();
}
void queue_activation(std::string id) { m_requestedActivation = std::move(id); }
void queue_back() { m_requestedBack = true; }
void queue_cycle(std::string id, int direction) {
m_requestedCycleId = std::move(id);
m_requestedCycleDirection = direction;
}
void add_button_control(Rml::Element* parent, std::string_view id, std::string_view text,
ButtonVariant variant) {
const std::string idString(id);
m_focusIds.push_back(idString);
m_buttons.push_back(std::make_unique<Button>(
parent, idString, text, variant, [this, idString] { queue_activation(idString); }));
}
void add_option_control(Rml::Element* parent, std::string_view id, std::string_view title,
std::string_view value, std::string_view detail) {
const std::string idString(id);
m_focusIds.push_back(idString);
m_options.push_back(
std::make_unique<GameOption>(parent, idString, title, value, detail,
[this, idString] { queue_activation(idString); }));
}
void add_disc_control(Rml::Element* parent) {
const std::string idString("select-disc");
m_focusIds.push_back(idString);
m_discState =
std::make_unique<DiscState>(parent, idString, disc_state_text(), !m_errorString.empty(),
[this, idString] { queue_activation(idString); });
}
void build_main(Rml::Element* screen) {
m_focusIds.clear();
m_buttons.clear();
m_discState.reset();
m_options.clear();
layout::add_brand(screen, logo_path(), m_compactLayout);
Rml::Element* panel = layout::add_panel(screen, false, m_compactLayout);
add_disc_control(panel);
if (is_selected_path_valid()) {
add_button_control(panel, "start", "Start Game", ButtonVariant::Primary);
}
add_button_control(panel, "options", "Options", ButtonVariant::Quiet);
}
AuroraBackend configured_backend() const {
AuroraBackend backend = BACKEND_AUTO;
if (!try_parse_backend(getSettings().backend.graphicsBackend.getValue(), backend)) {
backend = BACKEND_AUTO;
}
return backend;
}
std::string configured_backend_id() const {
return std::string(backend_id(configured_backend()));
}
std::string first_options_focus_id() const { return m_isPal ? "language" : "graphics-backend"; }
void build_options(Rml::Element* screen) {
m_focusIds.clear();
m_buttons.clear();
m_discState.reset();
m_options.clear();
layout::add_heading(screen, "Options");
Rml::Element* panel = layout::add_panel(screen, true);
if (m_isPal) {
const auto selectedLanguage = getSettings().game.language.getValue();
add_option_control(panel, "language", "Language",
kLanguageNames[static_cast<u8>(selectedLanguage)], "");
}
const AuroraBackend backend = configured_backend();
const std::string restartDetail =
getSettings().backend.graphicsBackend.getValue() != m_initialGraphicsBackend ?
"Restart required" :
"";
add_option_control(panel, "graphics-backend", "Graphics Backend", backend_name(backend),
restartDetail);
const auto fileType =
static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
add_option_control(panel, "save-file-type", "Save File Type", card_type_name(fileType), "");
add_button_control(panel, "back", "Back", ButtonVariant::Quiet);
}
void build_language_select(Rml::Element* screen) {
m_focusIds.clear();
m_buttons.clear();
m_discState.reset();
m_options.clear();
layout::add_heading(screen, "Language");
Rml::Element* panel = layout::add_panel(screen, true);
const auto selectedLanguage = getSettings().game.language.getValue();
for (size_t i = 0; i < kLanguageNames.size(); ++i) {
const std::string id = fmt::format("language-{}", i);
add_option_control(panel, id, kLanguageNames[i],
i == static_cast<size_t>(selectedLanguage) ? "Current" : "", "");
}
add_button_control(panel, "back", "Back", ButtonVariant::Quiet);
}
void build_graphics_backend_select(Rml::Element* screen) {
m_focusIds.clear();
m_buttons.clear();
m_discState.reset();
m_options.clear();
layout::add_heading(screen, "Graphics Backend");
Rml::Element* panel = layout::add_panel(screen, true);
const std::string currentBackendId = configured_backend_id();
for (const BackendChoice& choice : backend_choices()) {
const std::string id = "backend-" + choice.id;
add_option_control(panel, id, choice.name,
choice.id == currentBackendId ? "Current" : "", "");
}
add_button_control(panel, "back", "Back", ButtonVariant::Quiet);
}
void build_save_fileType_select(Rml::Element* screen) {
m_focusIds.clear();
m_buttons.clear();
m_discState.reset();
m_options.clear();
layout::add_heading(screen, "Save File Type");
Rml::Element* panel = layout::add_panel(screen, true);
const auto fileType =
static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
add_option_control(panel, "save-gci-folder", "GCI Folder",
fileType == CARD_GCIFOLDER ? "Current" : "", "");
add_option_control(panel, "save-card-image", "Card Image",
fileType == CARD_RAWIMAGE ? "Current" : "", "");
add_button_control(panel, "back", "Back", ButtonVariant::Quiet);
}
void rebuild() {
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
const std::string preferredFocus = choose_focus_after_rebuild();
close_document();
m_document = context->CreateDocument();
if (m_document == nullptr) {
return;
}
m_compactLayout = should_use_compact_layout();
const bool compactMainLayout = m_view == View::Main && m_compactLayout;
layout::style_document(m_document);
Rml::Element* screen =
layout::add_screen(m_document, compactMainLayout ? layout::ScreenLayout::CompactSplit :
layout::ScreenLayout::Standard);
if (m_view == View::Main) {
build_main(screen);
} else if (m_view == View::Options) {
build_options(screen);
} else if (m_view == View::LanguageSelect) {
build_language_select(screen);
} else if (m_view == View::GraphicsBackendSelect) {
build_graphics_backend_select(screen);
} else if (m_view == View::SaveFileTypeSelect) {
build_save_fileType_select(screen);
}
m_document->AddEventListener(Rml::EventId::Keydown, this);
m_document->Show();
focus_id(preferredFocus.empty() ? first_focus_id() : preferredFocus);
}
std::string choose_focus_after_rebuild() const {
if (!m_pendingFocusId.empty()) {
return m_pendingFocusId;
}
if (Rml::Context* context = aurora::rmlui::get_context()) {
if (Rml::Element* focused = context->GetFocusElement()) {
return focused->GetId();
}
}
return {};
}
std::string first_focus_id() const {
return m_focusIds.empty() ? std::string{} : m_focusIds.front();
}
int focus_index() const {
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return -1;
}
const Rml::Element* focused = context->GetFocusElement();
if (focused == nullptr) {
return -1;
}
const std::string& id = focused->GetId();
for (int i = 0; i < static_cast<int>(m_focusIds.size()); ++i) {
if (m_focusIds[i] == id) {
return i;
}
}
return -1;
}
void focus_id(std::string_view id) {
if (m_document == nullptr || id.empty()) {
return;
}
if (Rml::Element* element = m_document->GetElementById(std::string(id))) {
element->Focus(true);
m_pendingFocusId.clear();
}
}
void move_focus(int direction) {
if (m_focusIds.empty()) {
return;
}
int index = focus_index();
if (index < 0) {
focus_id(m_focusIds.front());
return;
}
const int nextIndex = index + direction;
if (nextIndex < 0 || nextIndex >= static_cast<int>(m_focusIds.size())) {
return;
}
focus_id(m_focusIds[static_cast<size_t>(nextIndex)]);
}
bool handle_key(Rml::Input::KeyIdentifier key) {
switch (key) {
case Rml::Input::KI_UP:
move_focus(-1);
return true;
case Rml::Input::KI_DOWN:
move_focus(1);
return true;
case Rml::Input::KI_LEFT:
queue_cycle_focused(-1);
return true;
case Rml::Input::KI_RIGHT:
queue_cycle_focused(1);
return true;
case Rml::Input::KI_RETURN:
queue_focused_activation();
return true;
case Rml::Input::KI_ESCAPE:
case Rml::Input::KI_F15:
queue_back();
return true;
default:
return false;
}
}
void queue_focused_activation() {
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
if (Rml::Element* focused = context->GetFocusElement()) {
queue_activation(focused->GetId());
}
}
void queue_cycle_focused(int direction) {
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
if (Rml::Element* focused = context->GetFocusElement()) {
queue_cycle(focused->GetId(), direction);
}
}
static void file_dialog_callback(void* userdata, const char* path, const char* error) {
auto* self = static_cast<Screen*>(userdata);
if (self == nullptr) {
return;
}
if (error != nullptr) {
self->m_selectedIsoPath.clear();
self->m_selectedIsoValid = false;
self->m_isPal = false;
self->m_errorString = fmt::format("File dialog error: {}", error);
self->m_pendingFocusId = "select-disc";
self->rebuild();
return;
}
if (path == nullptr) {
self->m_pendingFocusId =
self->m_view == View::Options ? self->first_options_focus_id() : "select-disc";
self->rebuild();
return;
}
self->m_selectedIsoPath = path;
self->validate_selected_iso(true);
self->m_pendingFocusId =
self->m_selectedIsoValid ?
(self->m_view == View::Options ? self->first_options_focus_id() : "start") :
(self->m_view == View::Options ? self->first_options_focus_id() : "select-disc");
self->rebuild();
}
void show_file_select() {
ShowFileSelect(&file_dialog_callback, this, aurora::window::get_sdl_window(),
kGameDiscFileFilters.data(), kGameDiscFileFilters.size(), nullptr, false);
}
void activate(std::string_view id) {
if (id == "start") {
if (is_selected_path_valid()) {
IsGameLaunched = true;
shutdown();
}
return;
}
if (id == "select-disc") {
show_file_select();
return;
}
if (id == "options") {
m_view = View::Options;
m_pendingFocusId = first_options_focus_id();
rebuild();
return;
}
if (id == "language" && m_isPal) {
m_view = View::LanguageSelect;
const auto selectedLanguage = getSettings().game.language.getValue();
m_pendingFocusId = fmt::format("language-{}", static_cast<int>(selectedLanguage));
rebuild();
return;
}
if (id == "graphics-backend") {
m_view = View::GraphicsBackendSelect;
m_pendingFocusId = "backend-" + configured_backend_id();
rebuild();
return;
}
if (id == "save-file-type") {
const auto fileType =
static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
m_view = View::SaveFileTypeSelect;
m_pendingFocusId = fileType == CARD_GCIFOLDER ? "save-gci-folder" : "save-card-image";
rebuild();
return;
}
if (id == "back") {
back();
return;
}
select_option(id);
}
void back() {
if (m_view == View::Options) {
m_view = View::Main;
m_pendingFocusId = is_selected_path_valid() ? "start" : "select-disc";
rebuild();
} else if (m_view == View::LanguageSelect) {
m_view = View::Options;
m_pendingFocusId = "language";
rebuild();
} else if (m_view == View::GraphicsBackendSelect) {
m_view = View::Options;
m_pendingFocusId = "graphics-backend";
rebuild();
} else if (m_view == View::SaveFileTypeSelect) {
m_view = View::Options;
m_pendingFocusId = "save-file-type";
rebuild();
}
}
void cycle_option(std::string_view id, int direction) {
if (m_view == View::LanguageSelect || m_view == View::GraphicsBackendSelect ||
m_view == View::SaveFileTypeSelect)
{
move_focus(direction);
}
}
void select_option(std::string_view id) {
const std::string idString(id);
if (idString.rfind("language-", 0) == 0 && m_isPal) {
const int languageIndex = std::stoi(idString.substr(std::string("language-").size()));
getSettings().game.language.setValue(static_cast<GameLanguage>(languageIndex));
Save();
m_view = View::Options;
m_pendingFocusId = "language";
rebuild();
return;
}
if (idString.rfind("backend-", 0) == 0) {
const std::string selectedBackend = idString.substr(std::string("backend-").size());
AuroraBackend backend = BACKEND_AUTO;
if (try_parse_backend(selectedBackend, backend)) {
getSettings().backend.graphicsBackend.setValue(selectedBackend);
Save();
}
m_view = View::Options;
m_pendingFocusId = "graphics-backend";
rebuild();
return;
}
if (id == "save-gci-folder" || id == "save-card-image") {
getSettings().backend.cardFileType.setValue(id == "save-gci-folder" ? CARD_GCIFOLDER :
CARD_RAWIMAGE);
Save();
m_view = View::Options;
m_pendingFocusId = "save-file-type";
rebuild();
}
}
};
Screen s_screen;
} // namespace
bool initialize() {
return s_screen.initialize();
}
void shutdown() {
s_screen.shutdown();
}
bool is_active() {
return s_screen.is_active();
}
void handle_event(const SDL_Event& event) {
s_screen.handle_event(event);
}
void update() {
s_screen.update();
}
} // namespace dusk::ui::prelaunch
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <SDL3/SDL_events.h>
namespace dusk::ui::prelaunch {
bool initialize();
void shutdown();
bool is_active();
void handle_event(const SDL_Event& event);
void update();
} // namespace dusk::ui::prelaunch
+19
View File
@@ -0,0 +1,19 @@
#include "theme.hpp"
#include <algorithm>
#include <fmt/format.h>
namespace dusk::ui::theme {
std::string rgba(Color color, int opacity) {
if (opacity >= 0) {
color.a = std::clamp(opacity, 0, 255);
}
return fmt::format("rgba({}, {}, {}, {})", color.r, color.g, color.b, color.a);
}
std::string dp(float value) {
return fmt::format("{}dp", value);
}
} // namespace dusk::ui::theme
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <string>
namespace dusk::ui::theme {
struct Color {
int r = 255;
int g = 255;
int b = 255;
int a = 255;
};
inline constexpr Color Background1{12, 18, 17, 255};
inline constexpr Color ModalOverlay{12, 18, 17, 229};
inline constexpr Color Text{225, 236, 231, 255};
inline constexpr Color TextActive{248, 255, 251, 255};
inline constexpr Color TextDim{160, 191, 182, 255};
inline constexpr Color Primary{69, 184, 170, 255};
inline constexpr Color PrimaryLight{135, 225, 211, 255};
inline constexpr Color Secondary{184, 113, 5, 255};
inline constexpr Color Elevated{160, 191, 182, 44};
inline constexpr Color ElevatedSoft{47, 56, 55, 154};
inline constexpr Color ElevatedBorder{160, 191, 182, 96};
inline constexpr Color Transparent{0, 0, 0, 0};
inline constexpr Color Danger{192, 30, 24, 255};
inline constexpr float BorderRadiusSmall = 8.0f;
inline constexpr float BorderRadiusMedium = 12.0f;
inline constexpr float BorderWidth = 1.1f;
std::string rgba(Color color, int opacity = -1);
std::string dp(float value);
} // namespace dusk::ui::theme
+244
View File
@@ -0,0 +1,244 @@
#include "ui.hpp"
#include <RmlUi/Core.h>
#include <RmlUi_Platform_SDL.h>
#include <SDL3/SDL_filesystem.h>
#include <aurora/rmlui.hpp>
#include <chrono>
#include <cmath>
#include <filesystem>
namespace dusk::ui {
namespace {
using Clock = std::chrono::steady_clock;
constexpr auto k_repeatStartDelay = std::chrono::milliseconds{500};
constexpr auto k_repeatRate = std::chrono::milliseconds{55};
constexpr float k_stickPressThreshold = 0.55f;
constexpr float k_stickReleaseThreshold = 0.18f;
bool s_initialized = false;
bool s_active = false;
bool s_controllerPrimary = false;
bool s_awaitStickReturnX = false;
bool s_awaitStickReturnY = false;
Rml::Input::KeyIdentifier sLatestControllerKey = Rml::Input::KI_UNKNOWN;
Clock::time_point sNextRepeatTime = {};
std::filesystem::path resource_path(const char* filename) {
const char* basePath = SDL_GetBasePath();
if (basePath == nullptr) {
return std::filesystem::path("res") / filename;
}
return std::filesystem::path(basePath) / "res" / filename;
}
void load_font(const char* filename, bool fallback = false) {
Rml::LoadFontFace(resource_path(filename).string(), fallback);
}
Rml::Context* context() {
return aurora::rmlui::get_context();
}
bool send_key_down(Rml::Input::KeyIdentifier key) {
if (key == Rml::Input::KI_UNKNOWN || context() == nullptr) {
return false;
}
context()->ProcessKeyDown(key, RmlSDL::GetKeyModifierState());
return true;
}
void send_controller_key(Rml::Input::KeyIdentifier key) {
if (!send_key_down(key)) {
return;
}
s_controllerPrimary = true;
sLatestControllerKey = key;
sNextRepeatTime = Clock::now() + k_repeatStartDelay;
}
Rml::Input::KeyIdentifier key_from_gamepad_button(Uint8 button) {
switch (button) {
case SDL_GAMEPAD_BUTTON_SOUTH:
case SDL_GAMEPAD_BUTTON_START:
return key_for_menu_action(MenuAction::Accept);
case SDL_GAMEPAD_BUTTON_WEST:
return key_for_menu_action(MenuAction::Back);
case SDL_GAMEPAD_BUTTON_BACK:
return key_for_menu_action(MenuAction::Toggle);
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
return key_for_menu_action(MenuAction::TabLeft);
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return key_for_menu_action(MenuAction::TabRight);
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return Rml::Input::KI_UP;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return Rml::Input::KI_DOWN;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return Rml::Input::KI_LEFT;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return Rml::Input::KI_RIGHT;
default:
return Rml::Input::KI_UNKNOWN;
}
}
Rml::Input::KeyIdentifier key_from_gamepad_axis(const SDL_GamepadAxisEvent& event,
float axisValue) {
switch (event.axis) {
case SDL_GAMEPAD_AXIS_LEFTX:
return axisValue < 0.0f ? Rml::Input::KI_LEFT : Rml::Input::KI_RIGHT;
case SDL_GAMEPAD_AXIS_LEFTY:
return axisValue < 0.0f ? Rml::Input::KI_UP : Rml::Input::KI_DOWN;
default:
return Rml::Input::KI_UNKNOWN;
}
}
} // namespace
bool initialize() {
if (s_initialized) {
return true;
}
if (!aurora::rmlui::is_initialized()) {
return false;
}
load_font("Inter-Regular.ttf");
load_font("Inter-Bold.ttf");
load_font("NotoMono-Regular.ttf", true);
s_initialized = true;
return true;
}
void shutdown() {
s_active = false;
s_initialized = false;
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
bool is_active() {
return s_active;
}
void set_active(bool active) {
s_active = active;
if (!active) {
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
}
Rml::Input::KeyIdentifier key_for_menu_action(MenuAction action) {
switch (action) {
case MenuAction::Accept:
return Rml::Input::KI_RETURN;
case MenuAction::Back:
return Rml::Input::KI_F15;
case MenuAction::Toggle:
return Rml::Input::KI_ESCAPE;
case MenuAction::TabLeft:
return Rml::Input::KI_F16;
case MenuAction::TabRight:
return Rml::Input::KI_F17;
case MenuAction::None:
default:
return Rml::Input::KI_UNKNOWN;
}
}
MenuAction menu_action_from_key(Rml::Input::KeyIdentifier key) {
if (key == key_for_menu_action(MenuAction::Accept)) {
return MenuAction::Accept;
}
if (key == key_for_menu_action(MenuAction::Back)) {
return MenuAction::Back;
}
if (key == key_for_menu_action(MenuAction::Toggle)) {
return MenuAction::Toggle;
}
if (key == key_for_menu_action(MenuAction::TabLeft)) {
return MenuAction::TabLeft;
}
if (key == key_for_menu_action(MenuAction::TabRight)) {
return MenuAction::TabRight;
}
return MenuAction::None;
}
void handle_event(const SDL_Event& event) {
if (!s_active || context() == nullptr) {
return;
}
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
if (!s_controllerPrimary || event.motion.xrel != 0.0f || event.motion.yrel != 0.0f) {
s_controllerPrimary = false;
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_WHEEL:
s_controllerPrimary = false;
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
send_controller_key(key_from_gamepad_button(event.gbutton.button));
break;
case SDL_EVENT_GAMEPAD_BUTTON_UP: {
const Rml::Input::KeyIdentifier key = key_from_gamepad_button(event.gbutton.button);
if (key == sLatestControllerKey) {
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
break;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
if (event.gaxis.axis != SDL_GAMEPAD_AXIS_LEFTX &&
event.gaxis.axis != SDL_GAMEPAD_AXIS_LEFTY)
{
break;
}
const float axisValue = static_cast<float>(event.gaxis.value) / 32768.0f;
bool& awaitStickReturn =
event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX ? s_awaitStickReturnX : s_awaitStickReturnY;
if (std::abs(axisValue) > k_stickPressThreshold) {
if (!awaitStickReturn) {
awaitStickReturn = true;
send_controller_key(key_from_gamepad_axis(event.gaxis, axisValue));
}
s_controllerPrimary = true;
} else if (awaitStickReturn && std::abs(axisValue) < k_stickReleaseThreshold) {
const Rml::Input::KeyIdentifier key = key_from_gamepad_axis(event.gaxis, axisValue);
if (key == sLatestControllerKey) {
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
awaitStickReturn = false;
}
break;
}
default:
break;
}
}
void update() {
if (!s_active || sLatestControllerKey == Rml::Input::KI_UNKNOWN || context() == nullptr) {
return;
}
const auto currentTime = Clock::now();
if (currentTime >= sNextRepeatTime) {
send_key_down(sLatestControllerKey);
sNextRepeatTime += k_repeatRate;
}
}
} // namespace dusk::ui
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <RmlUi/Core/Input.h>
#include <SDL3/SDL_events.h>
namespace dusk::ui {
enum class MenuAction {
None,
Accept,
Back,
Toggle,
TabLeft,
TabRight,
};
bool initialize();
void shutdown();
bool is_active();
void set_active(bool active);
void handle_event(const SDL_Event& event);
void update();
Rml::Input::KeyIdentifier key_for_menu_action(MenuAction action);
MenuAction menu_action_from_key(Rml::Input::KeyIdentifier key);
} // namespace dusk::ui
+13
View File
@@ -56,6 +56,7 @@
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/ui/prelaunch_screen.hpp"
#include "version.h"
#include <aurora/aurora.h>
@@ -134,17 +135,23 @@ AuroraStats dusk::lastFrameAuroraStats;
float dusk::frameUsagePct = 0.0f;
bool launchUILoop() {
const bool useRmlPrelaunch = dusk::ui::prelaunch::initialize();
while (dusk::IsRunning && !dusk::IsGameLaunched) {
const AuroraEvent* event = aurora_update();
while (event != nullptr && event->type != AURORA_NONE) {
switch (event->type) {
case AURORA_SDL_EVENT:
if (useRmlPrelaunch) {
dusk::ui::prelaunch::handle_event(event->sdl);
}
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
break;
case AURORA_DISPLAY_SCALE_CHANGED:
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
break;
case AURORA_EXIT:
dusk::ui::prelaunch::shutdown();
return false;
}
@@ -156,6 +163,10 @@ bool launchUILoop() {
continue;
}
if (useRmlPrelaunch) {
dusk::ui::prelaunch::update();
}
dusk::g_imguiConsole.PreDraw();
dusk::g_imguiConsole.PostDraw();
@@ -163,6 +174,8 @@ bool launchUILoop() {
aurora_end_frame();
}
dusk::ui::prelaunch::shutdown();
return dusk::IsRunning;
}