mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-23 14:41:33 -04:00
Revamped prelaunch experiment w/ RmlUi
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
- ### **[Official Website](https://twilitrealm.dev)**
|
||||
- ### **[Discord](https://discord.gg/QACynxeyna)**
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: b8407c6283...fa03533986
+20
@@ -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 |
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 += "&";
|
||||
break;
|
||||
case '<':
|
||||
result += "<";
|
||||
break;
|
||||
case '>':
|
||||
result += ">";
|
||||
break;
|
||||
case '"':
|
||||
result += """;
|
||||
break;
|
||||
case '\'':
|
||||
result += "'";
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user