Start UI over from scratch and add demo window

This commit is contained in:
Luke Street
2026-04-28 16:20:45 -06:00
parent f7b880c5ea
commit d899706208
35 changed files with 434 additions and 3745 deletions
+1 -1
+2 -22
View File
@@ -1462,28 +1462,8 @@ 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/control_surface.hpp
src/dusk/ui/control_surface.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_menu.hpp
src/dusk/ui/game_menu.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/editor.cpp
src/dusk/ui/editor.hpp
src/dusk/ui/ui.hpp
src/dusk/ui/ui.cpp
src/dusk/ui/window.hpp
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -4
View File
@@ -22,8 +22,6 @@
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "dusk/ui/game_menu.hpp"
#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"
@@ -341,14 +339,14 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
if (!dusk::IsGameLaunched && !dusk::ui::prelaunch::is_active()) {
if (!dusk::IsGameLaunched) {
m_preLaunchWindow.draw();
}
m_isHidden = !getSettings().backend.duskMenuOpen;
if (dusk::IsGameLaunched) {
if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
dusk::ui::game_menu::toggle();
m_isHidden = !m_isHidden;
}
if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack)) {
m_isHidden = !m_isHidden;
-152
View File
@@ -1,152 +0,0 @@
#include "button.hpp"
#include "control_surface.hpp"
#include "element.hpp"
#include "focus_border.hpp"
#include "label.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
#include <utility>
namespace dusk::ui {
namespace {
ControlSurfaceTone control_surface_tone(ButtonVariant variant) {
switch (variant) {
case ButtonVariant::Primary:
return ControlSurfaceTone::Primary;
case ButtonVariant::Secondary:
return ControlSurfaceTone::Secondary;
case ButtonVariant::Quiet:
default:
return ControlSurfaceTone::Quiet;
}
}
} // 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,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::Height, rml_dp(68.0f)},
{Rml::PropertyId::MinHeight, rml_dp(68.0f)},
{Rml::PropertyId::MaxHeight, rml_dp(68.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(22.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(22.0f)},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::Cursor, rml_string("pointer")},
{Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto},
{Rml::PropertyId::NavUp, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavDown, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavLeft, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavRight, Rml::Style::Nav::Auto},
{Rml::PropertyId::Opacity, rml_number(1.0f)},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
{Rml::PropertyId::Color, rml_color(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, {
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::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_hovered = true;
apply_style();
break;
case Rml::EventId::Mouseout:
m_hovered = 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;
apply_control_surface_style(
m_element, control_surface_style(control_surface_tone(m_variant)), active);
m_element->SetProperty(Rml::PropertyId::Color, rml_color(active ? TextActive : Text));
m_label->SetProperty(Rml::PropertyId::Color, rml_color(active ? TextActive : Text));
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
-47
View File
@@ -1,47 +0,0 @@
#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
-60
View File
@@ -1,60 +0,0 @@
#include "control_surface.hpp"
#include <RmlUi/Core/Element.h>
namespace dusk::ui {
ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone) {
switch (tone) {
case ControlSurfaceTone::Primary:
return {
.accent = theme::Primary,
.inactiveBorderOpacity = 112,
.inactiveBackgroundOpacity = 28,
.activeBorderOpacity = 255,
.activeBackgroundOpacity = 116,
};
case ControlSurfaceTone::Secondary:
return {
.accent = theme::Secondary,
.inactiveBorderOpacity = 112,
.inactiveBackgroundOpacity = 28,
.activeBorderOpacity = 255,
.activeBackgroundOpacity = 116,
};
case ControlSurfaceTone::Window:
return {
.accent = theme::WindowAccent,
.inactiveBorderOpacity = 0,
.inactiveBackgroundOpacity = 26,
.activeBorderOpacity = 200,
.activeBackgroundOpacity = 76,
};
case ControlSurfaceTone::Quiet:
default:
return {
.accent = theme::Elevated,
.inactiveBorderOpacity = 86,
.inactiveBackgroundOpacity = 0,
.activeBorderOpacity = 150,
.activeBackgroundOpacity = 68,
};
}
}
void apply_control_surface_style(
Rml::Element* element, const ControlSurfaceStyle& style, bool active) {
if (element == nullptr) {
return;
}
const auto borderColor = active ? rml_color(style.accent, style.activeBorderOpacity) :
rml_color(theme::ElevatedBorder, style.inactiveBorderOpacity);
element->SetProperty(Rml::PropertyId::BorderLeftColor, borderColor);
element->SetProperty(Rml::PropertyId::BorderRightColor, borderColor);
element->SetProperty(Rml::PropertyId::BorderTopColor, borderColor);
element->SetProperty(Rml::PropertyId::BorderBottomColor, borderColor);
element->SetProperty(Rml::PropertyId::BackgroundColor,
rml_color(style.accent,
active ? style.activeBackgroundOpacity : style.inactiveBackgroundOpacity));
}
} // namespace dusk::ui
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include "theme.hpp"
namespace Rml {
class Element;
}
namespace dusk::ui {
enum class ControlSurfaceTone {
Primary,
Secondary,
Quiet,
Window,
};
struct ControlSurfaceStyle {
theme::Color accent = theme::Primary;
int inactiveBorderOpacity = 86;
int inactiveBackgroundOpacity = 0;
int activeBorderOpacity = 150;
int activeBackgroundOpacity = 68;
};
ControlSurfaceStyle control_surface_style(ControlSurfaceTone tone);
void apply_control_surface_style(
Rml::Element* element, const ControlSurfaceStyle& style, bool active);
} // namespace dusk::ui
-153
View File
@@ -1,153 +0,0 @@
#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,
std::string_view statusText, bool statusIsError, std::function<void()> pressedCallback)
: m_pressedCallback(std::move(pressedCallback)), m_statusIsError(statusIsError) {
using namespace theme;
m_element = append(parent, "button", id,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Stretch},
{Rml::PropertyId::RowGap, rml_dp(6.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(6.0f)},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::PaddingTop, rml_dp(14.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(16.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(14.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(16.0f)},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::Cursor, rml_string("pointer")},
{Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto},
{Rml::PropertyId::NavUp, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavDown, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavLeft, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavRight, Rml::Style::Nav::Auto},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
});
add_focus_border(m_element, BorderRadiusSmall);
m_value = add_label(m_element, text, LabelStyle::Body);
set_props(m_value, {
{Rml::PropertyId::OverflowX, Rml::Style::Overflow::Hidden},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Hidden},
{Rml::PropertyId::TextOverflow, Rml::Style::TextOverflow::Ellipsis},
{Rml::PropertyId::WhiteSpace, Rml::Style::WhiteSpace::Nowrap},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
});
if (!statusText.empty()) {
m_status = add_label(m_element, statusText, LabelStyle::Annotation);
set_props(m_status, {
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
{Rml::PropertyId::WhiteSpace, Rml::Style::WhiteSpace::Normal},
});
}
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_statusIsError ? Danger : Primary;
m_element->SetProperty(Rml::PropertyId::BackgroundColor,
rml_color(accent, active ? 52 : (m_statusIsError ? 32 : 20)));
const auto borderColor = rml_color(accent, active ? 220 : (m_statusIsError ? 190 : 120));
m_element->SetProperty(Rml::PropertyId::BorderTopColor, borderColor);
m_element->SetProperty(Rml::PropertyId::BorderRightColor, borderColor);
m_element->SetProperty(Rml::PropertyId::BorderBottomColor, borderColor);
m_element->SetProperty(Rml::PropertyId::BorderLeftColor, borderColor);
m_element->SetProperty(Rml::PropertyId::Color, rml_color(active ? TextActive : Text));
m_value->SetProperty(Rml::PropertyId::Color, rml_color(active ? TextActive : Text));
if (m_status != nullptr) {
m_status->SetProperty(
Rml::PropertyId::Color, rml_color(m_statusIsError ? Danger : TextDim));
}
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
-41
View File
@@ -1,41 +0,0 @@
#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,
std::string_view statusText, bool statusIsError, 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_value = nullptr;
Rml::Element* m_status = nullptr;
std::function<void()> m_pressedCallback;
bool m_statusIsError = false;
bool m_hovered = false;
bool m_focused = false;
void apply_style();
};
} // namespace dusk::ui
+112
View File
@@ -0,0 +1,112 @@
#include "editor.hpp"
#include <RmlUi/Core.h>
namespace dusk::ui {
namespace {
const Rml::String kPlayerStatusContent = R"RML(
<div class="pane">
<div class="section-heading">Player</div>
<button class="select-button">
<div class="key">Player Name</div>
<div class="value">Link</div>
</button>
<button class="select-button">
<div class="key">Horse Name</div>
<div class="value">Epona</div>
</button>
<button class="select-button">
<div class="key">Max Health</div>
<div class="value">15</div>
</button>
<button class="select-button">
<div class="key">Health</div>
<div class="value">12</div>
</button>
<button class="select-button">
<div class="key">Max Oil</div>
<div class="value">0</div>
</button>
<button class="select-button">
<div class="key">Oil</div>
<div class="value">0</div>
</button>
<div class="section-heading">Equipment</div>
<button class="select-button selected">
<div class="key">Equip X</div>
<div class="value">Spinner</div>
</button>
<button class="select-button">
<div class="key">Equip Y</div>
<div class="value">None</div>
</button>
<button class="select-button">
<div class="key">Combo Equip X</div>
<div class="value">None</div>
</button>
<button class="select-button">
<div class="key">Combo Equip Y</div>
<div class="value">None</div>
</button>
<button class="select-button">
<div class="key">Clothes</div>
<div class="value">Hero's Clothes</div>
</button>
</div>
<!-- TODO: right pane is going to be dynamic based on the highlighted left pane value -->
<div class="pane">
<button class="button">Slot 0 (Gale Boomerang)</button>
<button class="button">Slot 1 (Lantern)</button>
<button class="button">Slot 2 (Spinner)</button>
<button class="button">Slot 3 (Iron Boots)</button>
<button class="button">Slot 4 (Hero's Bow)</button>
<button class="button">Slot 5 (Hawkeye)</button>
<button class="button">Slot 6 (Ball and Chain)</button>
<button class="button">Slot 7 (None)</button>
<button class="button">Slot 8 (Dominion Rod)</button>
</div>
)RML";
const Rml::String kLocationContent = R"RML(
<div class="pane">
<div class="section-heading">Save Location</div>
<button class="select-button">
<div class="key">Stage</div>
<div class="value">F_SP108</div>
</button>
<button class="select-button">
<div class="key">Room</div>
<div class="value">1</div>
</button>
<button class="select-button">
<div class="key">Spawn ID</div>
<div class="value">0</div>
</button>
<div class="section-heading">Horse Location</div>
<button class="select-button">
<div class="key">Position</div>
<div class="value">34814, -260, -41181</div>
</button>
</div>
<div class="pane"></div>
)RML";
} // namespace
EditorWindow::EditorWindow()
: Window({.tabs = {
{"Player Status",
[](Rml::Element* content) {
// TODO: actually bind values and events. wonder if we should have
// a SettingsPane element or something for sharing?
Rml::Factory::InstanceElementText(content, kPlayerStatusContent);
}},
{"Location",
[](Rml::Element* content) {
Rml::Factory::InstanceElementText(content, kLocationContent);
}},
{"Inventory"},
}}) {}
} // namespace dusk::ui
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "window.hpp"
namespace dusk::ui {
class EditorWindow : public Window {
public:
EditorWindow();
};
} // namespace dusk::ui
-94
View File
@@ -1,94 +0,0 @@
#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,
std::initializer_list<std::pair<Rml::PropertyId, Rml::Property> > properties) {
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));
}
Rml::Element* appended = parent->AppendChild(std::move(child));
set_props(appended, properties);
return appended;
}
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));
}
}
void set_props(Rml::Element* element,
std::initializer_list<std::pair<Rml::PropertyId, Rml::Property> > properties) {
if (element == nullptr) {
return;
}
for (const auto& [name, value] : properties) {
element->SetProperty(name, value);
}
}
} // namespace dusk::ui
-21
View File
@@ -1,21 +0,0 @@
#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 = {},
std::initializer_list<std::pair<Rml::PropertyId, Rml::Property> > properties = {});
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);
void set_props(Rml::Element* element,
std::initializer_list<std::pair<Rml::PropertyId, Rml::Property> > properties);
} // namespace dusk::ui
-51
View File
@@ -1,51 +0,0 @@
#include "focus_border.hpp"
#include "element.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
namespace dusk::ui {
Rml::Element* add_focus_border(Rml::Element* parent, float radius) {
using namespace theme;
const auto borderColor = rml_color(PrimaryLight, 0);
return append(parent, "div", {},
{
{Rml::PropertyId::Position, Rml::Style::Position::Absolute},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
{Rml::PropertyId::Left, rml_dp(-(BorderWidth * 3.0f))},
{Rml::PropertyId::Top, rml_dp(-(BorderWidth * 3.0f))},
{Rml::PropertyId::Right, rml_dp(-(BorderWidth * 3.0f))},
{Rml::PropertyId::Bottom, rml_dp(-(BorderWidth * 3.0f))},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth * 2.0f)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth * 2.0f)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth * 2.0f)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth * 2.0f)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(radius + BorderWidth * 4.0f)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(radius + BorderWidth * 4.0f)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(radius + BorderWidth * 4.0f)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(radius + BorderWidth * 4.0f)},
{Rml::PropertyId::BorderTopColor, borderColor},
{Rml::PropertyId::BorderRightColor, borderColor},
{Rml::PropertyId::BorderBottomColor, borderColor},
{Rml::PropertyId::BorderLeftColor, borderColor},
});
}
void set_focus_border_visible(Rml::Element* parent, bool visible) {
if (parent == nullptr || parent->GetNumChildren() == 0) {
return;
}
const auto borderColor = rml_color(theme::PrimaryLight, visible ? 255 : 0);
set_props(parent->GetChild(0), {
{Rml::PropertyId::BorderTopColor, borderColor},
{Rml::PropertyId::BorderRightColor, borderColor},
{Rml::PropertyId::BorderBottomColor, borderColor},
{Rml::PropertyId::BorderLeftColor, borderColor},
});
}
} // namespace dusk::ui
-10
View File
@@ -1,10 +0,0 @@
#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
-808
View File
@@ -1,808 +0,0 @@
#include "game_menu.hpp"
#include "element.hpp"
#include "game_option.hpp"
#include "label.hpp"
#include "theme.hpp"
#include "ui.hpp"
#include "window.hpp"
#include "dusk/config.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "m_Do/m_Do_graphic.h"
#include <RmlUi/Core.h>
#include <RmlUi/Core/ElementDocument.h>
#include <aurora/aurora.h>
#include <aurora/gfx.h>
#include <aurora/rmlui.hpp>
#include <dolphin/vi.h>
#include <fmt/format.h>
#include <algorithm>
#include <array>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace aurora::gx {
extern bool enableLodBias;
}
namespace dusk::ui::game_menu {
namespace {
enum class Tab : int {
Audio = 0,
Cheats,
Gameplay,
Graphics,
Input,
Interface,
Count,
};
struct TabDef {
const char* id;
const char* label;
};
constexpr std::array<TabDef, static_cast<size_t>(Tab::Count)> kTabs{{
{"audio", "Audio"},
{"cheats", "Cheats"},
{"gameplay", "Gameplay"},
{"graphics", "Graphics"},
{"input", "Input"},
{"interface", "Interface"},
}};
constexpr int kInternalResolutionScaleMax = 12;
constexpr int kShadowResolutionMax = 8;
constexpr std::array<float, 5> kBloomMultiplierStops{0.0f, 0.25f, 0.50f, 0.75f, 1.00f};
constexpr std::array<const char*, 3> kBloomModeNames{"Off", "Classic", "Dusk"};
// TODO: Needs more spacing for newlines
static const char* get_description_for_item(std::string_view id) {
if (id == "internal-resolution") {
return "Auto renders at the native window resolution.\nHigher values scale the internal "
"framebuffer.";
}
if (id == "shadow-resolution") {
return "Improves the shadow resolution, making them higher quality.";
}
if (id == "frame-interp") {
return "Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, "
"animation glitches, or instability may occur.";
}
return "No description found.";
}
struct Row {
std::string id;
std::function<void()> activate;
std::function<void(int direction)> cycle;
};
class Screen : public Rml::EventListener {
public:
bool initialize() {
if (m_initialized) {
return true;
}
if (!ui::initialize()) {
return false;
}
m_initialized = true;
return true;
}
void shutdown() {
close_document();
if (ui::is_active()) {
ui::set_active(false);
}
m_initialized = false;
m_focusIds.clear();
m_rows.clear();
}
bool is_open() const { return m_open; }
void set_open(bool open) {
if (open == m_open) {
return;
}
if (open && !initialize()) {
return;
}
m_open = open;
if (open) {
ui::set_active(true);
rebuild();
} else {
close_document();
ui::set_active(false);
}
}
void toggle() { set_open(!m_open); }
void handle_event(const SDL_Event& event) {
if (!m_open) {
return;
}
ui::handle_event(event);
}
void update() {
if (!m_open) {
return;
}
if (m_requestClose) {
m_requestClose = false;
set_open(false);
return;
}
if (!m_pendingTabId.empty()) {
const std::string tabId = std::move(m_pendingTabId);
m_pendingTabId.clear();
apply_tab_selection(tabId);
if (!m_open) {
return;
}
}
if (!m_requestedActivation.empty()) {
const std::string id = std::move(m_requestedActivation);
m_requestedActivation.clear();
invoke_activate(id);
if (!m_open) {
return;
}
}
if (m_requestedCycleDirection != 0) {
const std::string id = std::move(m_requestedCycleId);
const int direction = m_requestedCycleDirection;
m_requestedCycleId.clear();
m_requestedCycleDirection = 0;
invoke_cycle(id, direction);
if (!m_open) {
return;
}
}
if (m_needsRebuild) {
m_needsRebuild = false;
rebuild();
}
ui::update();
sync_description_pane();
}
void ProcessEvent(Rml::Event& event) override {
if (event.GetId() != Rml::EventId::Keydown) {
return;
}
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;
bool m_open = false;
Tab m_tab = Tab::Graphics;
Rml::ElementDocument* m_document = nullptr;
std::unique_ptr<Window> m_window;
std::vector<std::unique_ptr<GameOption> > m_options;
std::vector<Row> m_rows;
std::vector<std::string> m_focusIds;
std::string m_pendingFocusId;
std::string m_requestedActivation;
std::string m_requestedCycleId;
int m_requestedCycleDirection = 0;
bool m_requestClose = false;
bool m_needsRebuild = false;
std::string m_pendingTabId;
Rml::Element* m_descriptionElement = nullptr;
Rml::Element* m_lastDescriptionSyncFocus = nullptr;
Row* find_row(std::string_view id) {
for (auto& row : m_rows) {
if (row.id == id) {
return &row;
}
}
return nullptr;
}
void invoke_activate(const std::string& id) {
if (Row* row = find_row(id); row && row->activate) {
row->activate();
}
}
void invoke_cycle(const std::string& id, int direction) {
if (Row* row = find_row(id); row && row->cycle) {
row->cycle(direction);
}
}
void close_document() {
if (m_document == nullptr) {
return;
}
m_document->RemoveEventListener(Rml::EventId::Keydown, this);
m_options.clear();
m_rows.clear();
m_focusIds.clear();
m_descriptionElement = nullptr;
m_lastDescriptionSyncFocus = nullptr;
m_window.reset();
m_document->Close();
m_document = nullptr;
}
void style_document(Rml::ElementDocument* document) {
using namespace theme;
set_props(document, {
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::Height, rml_percent(100.0f)},
{Rml::PropertyId::MarginTop, rml_px(0.0f)},
{Rml::PropertyId::MarginRight, rml_px(0.0f)},
{Rml::PropertyId::MarginBottom, rml_px(0.0f)},
{Rml::PropertyId::MarginLeft, rml_px(0.0f)},
{Rml::PropertyId::PaddingTop, rml_px(0.0f)},
{Rml::PropertyId::PaddingRight, rml_px(0.0f)},
{Rml::PropertyId::PaddingBottom, rml_px(0.0f)},
{Rml::PropertyId::PaddingLeft, rml_px(0.0f)},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
{Rml::PropertyId::Color, rml_color(Text)},
});
}
Rml::Element* add_screen() {
using namespace theme;
return append(m_document, "div", "game-menu-screen",
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Absolute},
{Rml::PropertyId::Left, rml_px(0.0f)},
{Rml::PropertyId::Top, rml_px(0.0f)},
{Rml::PropertyId::Right, rml_px(0.0f)},
{Rml::PropertyId::Bottom, rml_px(0.0f)},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::PaddingTop, rml_dp(32.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(32.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(32.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(32.0f)},
});
}
Rml::Element* add_section_header(Rml::Element* parent, std::string_view title) {
auto* row = append(parent, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::PaddingTop, rml_dp(8.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(4.0f)},
});
auto* label = add_label(row, title, LabelStyle::Annotation);
set_props(label, {
{Rml::PropertyId::FontSize, rml_dp(14.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(3.0f)},
{Rml::PropertyId::Color, rml_color(theme::WindowAccentSoft)},
{Rml::PropertyId::FlexShrink, rml_number(0.0f)},
});
return row;
}
Rml::Element* add_scroll_body(Rml::Element* parent) {
return append(parent, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::MinHeight, rml_px(0.0f)},
{Rml::PropertyId::RowGap, rml_dp(8.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(8.0f)},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Auto},
});
}
std::function<void()> queue_activate(std::string id) {
return [this, id = std::move(id)] { m_requestedActivation = id; };
}
void register_row(Row row, std::unique_ptr<GameOption> option) {
m_focusIds.push_back(row.id);
m_rows.push_back(std::move(row));
m_options.push_back(std::move(option));
}
void add_toggle(Rml::Element* parent, std::string id, std::string_view title,
config::ConfigVar<bool>& var, std::function<void(bool)> sideEffect = {},
std::string_view detail = {}) {
auto mutate = [this, &var, sideEffect = std::move(sideEffect)] {
const bool next = !var.getValue();
var.setValue(next);
Save();
if (sideEffect) {
sideEffect(next);
}
m_needsRebuild = true;
};
const std::string_view valueText = var.getValue() ? "On" : "Off";
auto option =
std::make_unique<GameOption>(parent, id, title, valueText, detail, queue_activate(id));
register_row(Row{id, mutate, [mutate](int) { mutate(); }}, std::move(option));
}
void add_action(Rml::Element* parent, std::string id, std::string_view title,
std::function<void()> action, std::string_view valueText = ">",
std::string_view detail = {}) {
auto mutate = [this, action = std::move(action)] {
action();
m_needsRebuild = true;
};
auto option =
std::make_unique<GameOption>(parent, id, title, valueText, detail, queue_activate(id));
register_row(Row{id, mutate, {}}, std::move(option));
}
template <typename T>
void add_cycle_row(Rml::Element* parent, std::string id, std::string_view title,
std::string_view valueText, std::string_view detail, std::function<void(int)> cycle) {
auto mutate = [this, cycle] {
cycle(1);
m_needsRebuild = true;
};
auto cycleWithRebuild = [this, cycle](int direction) {
cycle(direction);
m_needsRebuild = true;
};
auto option =
std::make_unique<GameOption>(parent, id, title, valueText, detail, queue_activate(id));
register_row(Row{id, mutate, cycleWithRebuild}, std::move(option));
}
void add_int_cycle(Rml::Element* parent, std::string id, std::string_view title,
config::ConfigVar<int>& var, int minValue, int maxValue,
std::function<std::string(int)> formatter, std::function<void(int)> sideEffect = {}) {
const int current = std::clamp(var.getValue(), minValue, maxValue);
const std::string valueText = formatter(current);
auto cycle = [&var, minValue, maxValue, sideEffect = std::move(sideEffect)](int dir) {
int next = std::clamp(var.getValue(), minValue, maxValue) + dir;
if (next < minValue) {
next = maxValue;
} else if (next > maxValue) {
next = minValue;
}
var.setValue(next);
Save();
if (sideEffect) {
sideEffect(next);
}
};
add_cycle_row<int>(parent, std::move(id), title, valueText, {}, std::move(cycle));
}
void add_bloom_mode_row(Rml::Element* parent) {
auto& var = getSettings().game.bloomMode;
const int current = std::clamp(
static_cast<int>(var.getValue()), 0, static_cast<int>(kBloomModeNames.size() - 1));
const std::string_view valueText = kBloomModeNames[static_cast<size_t>(current)];
auto cycle = [&var](int dir) {
const int count = kBloomModeNames.size();
int next = static_cast<int>(var.getValue()) + dir;
next = (next % count + count) % count;
var.setValue(static_cast<BloomMode>(next));
Save();
};
add_cycle_row<int>(parent, "bloom-mode", "Bloom", valueText, {}, std::move(cycle));
}
void add_bloom_brightness_row(Rml::Element* parent) {
auto& var = getSettings().game.bloomMultiplier;
const std::string valueText = fmt::format("{:.2f}", var.getValue());
auto cycle = [&var](int dir) {
const float currentValue = var.getValue();
int closest = 0;
float bestDelta = std::abs(currentValue - kBloomMultiplierStops[0]);
for (int i = 1; i < static_cast<int>(kBloomMultiplierStops.size()); ++i) {
const float delta = std::abs(currentValue - kBloomMultiplierStops[i]);
if (delta < bestDelta) {
bestDelta = delta;
closest = i;
}
}
const int count = kBloomMultiplierStops.size();
const int next = (closest + dir + count) % count;
var.setValue(kBloomMultiplierStops[next]);
Save();
};
const std::string_view detail =
getSettings().game.bloomMode.getValue() == BloomMode::Off ? "Bloom is disabled" : "";
add_cycle_row<int>(
parent, "bloom-brightness", "Bloom Brightness", valueText, detail, std::move(cycle));
}
void build_description_pane() {
m_descriptionElement = nullptr;
m_lastDescriptionSyncFocus = nullptr;
Rml::Element* right = m_window->right_pane();
if (right == nullptr) {
return;
}
m_descriptionElement = append_text(right, "p", " ", "option-description");
set_props(m_descriptionElement,
{
{Rml::PropertyId::Color, rml_color(theme::TextActive)},
{Rml::PropertyId::FontSize, rml_dp(20.0f)},
{Rml::PropertyId::LineHeight, Rml::Property(1.45f, Rml::Unit::EM)},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Left},
{Rml::PropertyId::AlignSelf, Rml::Style::AlignSelf::Stretch},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::WhiteSpace, Rml::Style::WhiteSpace::Preline},
});
}
void sync_description_pane() {
if (m_descriptionElement == nullptr) {
return;
}
if (m_tab != Tab::Graphics) {
return;
}
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
Rml::Element* const focused = context->GetFocusElement();
if (focused == m_lastDescriptionSyncFocus) {
return;
}
m_lastDescriptionSyncFocus = focused;
if (focused == nullptr) {
set_text(m_descriptionElement, get_description_for_item({}));
} else {
set_text(m_descriptionElement, get_description_for_item(focused->GetId()));
}
}
void build_graphics_tab(Rml::Element* body) {
auto* scroll = add_scroll_body(body);
add_section_header(scroll, "Display");
// TODO: Replace this with a Display Mode toggle.
add_toggle(scroll, "fullscreen", "Toggle Fullscreen", getSettings().video.enableFullscreen,
[](bool enabled) { VISetWindowFullscreen(enabled); });
u32 internalWidth = 0;
u32 internalHeight = 0;
AuroraGetRenderSize(&internalWidth, &internalHeight);
const std::string detail = fmt::format("Current: {}x{}", internalWidth, internalHeight);
const int currentScale = std::clamp(
getSettings().game.internalResolutionScale.getValue(), 0, kInternalResolutionScaleMax);
const std::string scaleValue =
currentScale == 0 ? std::string("Auto") : fmt::format("{}x", currentScale);
auto scaleCycle = [](int dir) {
int next = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0,
kInternalResolutionScaleMax) +
dir;
if (next < 0) {
next = kInternalResolutionScaleMax;
} else if (next > kInternalResolutionScaleMax) {
next = 0;
}
getSettings().game.internalResolutionScale.setValue(next);
VISetFrameBufferScale(static_cast<float>(next));
Save();
};
add_cycle_row<int>(scroll, "internal-resolution", "Internal Resolution", scaleValue, detail,
std::move(scaleCycle));
add_int_cycle(scroll, "shadow-resolution", "Shadow Resolution",
getSettings().game.shadowResolutionMultiplier, 1, kShadowResolutionMax,
[](int v) { return fmt::format("x{}", v); });
add_toggle(scroll, "lock-aspect", "Force 4:3 Aspect Ratio",
getSettings().video.lockAspectRatio, [](bool enabled) {
AuroraSetViewportPolicy(enabled ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
});
add_toggle(scroll, "vsync", "VSync", getSettings().video.enableVsync,
[](bool enabled) { aurora_enable_vsync(enabled); });
add_toggle(scroll, "frame-interp", "Unlock Framerate",
getSettings().game.enableFrameInterpolation, {}, "Experimental");
add_section_header(scroll, "Post-Processing");
add_bloom_mode_row(scroll);
if (getSettings().game.bloomMode.getValue() != BloomMode::Off) {
add_bloom_brightness_row(scroll);
}
add_toggle(
scroll, "depth-of-field", "Depth of Field", getSettings().game.enableDepthOfField);
add_section_header(scroll, "Developer Options");
const std::string lodValue = aurora::gx::enableLodBias ? "On" : "Off";
auto lodMutate = [this] {
aurora::gx::enableLodBias = !aurora::gx::enableLodBias;
m_needsRebuild = true;
};
auto lodOption = std::make_unique<GameOption>(scroll, "lod-bias", "LOD Bias", lodValue,
std::string_view{}, queue_activate("lod-bias"));
register_row(
Row{"lod-bias", lodMutate, [lodMutate](int) { lodMutate(); }}, std::move(lodOption));
add_toggle(
scroll, "minimap-shadows", "Mini-Map Shadows", getSettings().game.enableMapBackground);
}
void build_placeholder_tab(Rml::Element* body, std::string_view tabLabel) {
auto* wrap = append(body, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::RowGap, rml_dp(12.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(12.0f)},
});
auto* heading = add_label(wrap, tabLabel, LabelStyle::Large);
set_props(heading, {{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Center}});
auto* sub = add_label(wrap, "Not yet ported.", LabelStyle::Body);
set_props(sub, {
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Center},
{Rml::PropertyId::Color, rml_color(theme::TextDim)},
});
}
void build_body() {
m_window->set_right_pane_visible(m_tab == Tab::Graphics);
Rml::Element* body = m_window->body();
switch (m_tab) {
case Tab::Graphics:
build_graphics_tab(body);
build_description_pane();
break;
default:
build_placeholder_tab(body, kTabs[static_cast<size_t>(m_tab)].label);
break;
}
}
void rebuild() {
if (!m_open) {
return;
}
Rml::Context* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
const std::string preferredFocus =
m_pendingFocusId.empty() ? current_focus_id() : m_pendingFocusId;
m_pendingFocusId.clear();
close_document();
m_document = context->CreateDocument();
if (m_document == nullptr) {
return;
}
style_document(m_document);
Rml::Element* screen = add_screen();
m_window = std::make_unique<Window>(screen, "game-menu", [this] { request_close(); });
for (const TabDef& tab : kTabs) {
const std::string tabId = tab.id;
m_window->add_tab(tabId, tab.label, [this, tabId] { m_pendingTabId = tabId; });
}
m_window->set_selected_tab(kTabs[static_cast<size_t>(m_tab)].id);
build_body();
m_document->AddEventListener(Rml::EventId::Keydown, this);
m_document->Show();
focus_id(preferredFocus.empty() ? first_focus_id() : preferredFocus);
sync_description_pane();
}
void request_close() { m_requestClose = true; }
void switch_tab(int direction) {
const int count = kTabs.size();
const int next = (static_cast<int>(m_tab) + direction + count) % count;
m_pendingTabId = kTabs[static_cast<size_t>(next)].id;
}
void apply_tab_selection(std::string_view tabId) {
for (size_t i = 0; i < kTabs.size(); ++i) {
if (tabId == kTabs[i].id) {
if (m_tab == static_cast<Tab>(i)) {
return;
}
m_tab = static_cast<Tab>(i);
m_pendingFocusId.clear();
rebuild();
return;
}
}
}
std::string current_focus_id() const {
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 {
const std::string id = current_focus_id();
if (id.empty()) {
return -1;
}
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) {
return;
}
if (!id.empty()) {
if (Rml::Element* element = m_document->GetElementById(std::string(id))) {
element->Focus(true);
return;
}
}
const std::string fallback = first_focus_id();
if (!fallback.empty()) {
if (Rml::Element* element = m_document->GetElementById(fallback)) {
element->Focus(true);
}
}
}
void move_focus(int direction) {
if (m_focusIds.empty()) {
return;
}
const int index = focus_index();
if (index < 0) {
focus_id(m_focusIds.front());
return;
}
const int next = index + direction;
if (next < 0 || next >= static_cast<int>(m_focusIds.size())) {
return;
}
focus_id(m_focusIds[static_cast<size_t>(next)]);
}
void queue_activate_focused() {
const std::string id = current_focus_id();
if (!id.empty()) {
m_requestedActivation = id;
}
}
void queue_cycle_focused(int direction) {
const std::string id = current_focus_id();
if (!id.empty()) {
m_requestedCycleId = id;
m_requestedCycleDirection = direction;
}
}
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_F16:
switch_tab(-1);
return true;
case Rml::Input::KI_F17:
switch_tab(1);
return true;
case Rml::Input::KI_RETURN:
queue_activate_focused();
return true;
case Rml::Input::KI_ESCAPE:
case Rml::Input::KI_F15:
request_close();
return true;
default:
return false;
}
}
};
Screen s_screen;
} // namespace
bool initialize() {
return s_screen.initialize();
}
void shutdown() {
s_screen.shutdown();
}
bool is_active() {
return s_screen.is_open();
}
void toggle() {
s_screen.toggle();
}
void set_active(bool active) {
s_screen.set_open(active);
}
void handle_event(const SDL_Event& event) {
s_screen.handle_event(event);
}
void update() {
s_screen.update();
}
} // namespace dusk::ui::game_menu
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include <SDL3/SDL_events.h>
namespace dusk::ui::game_menu {
bool initialize();
void shutdown();
bool is_active();
void toggle();
void set_active(bool active);
void handle_event(const SDL_Event& event);
void update();
} // namespace dusk::ui::game_menu
-201
View File
@@ -1,201 +0,0 @@
#include "game_option.hpp"
#include "control_surface.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,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::SpaceBetween},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::RowGap, rml_dp(16.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(16.0f)},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::PaddingTop, rml_dp(16.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(16.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(16.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(16.0f)},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BackgroundColor, rml_color(Transparent)},
{Rml::PropertyId::BorderTopColor, rml_color(ElevatedBorder, 0)},
{Rml::PropertyId::BorderRightColor, rml_color(ElevatedBorder, 0)},
{Rml::PropertyId::BorderBottomColor, rml_color(ElevatedBorder, 0)},
{Rml::PropertyId::BorderLeftColor, rml_color(ElevatedBorder, 0)},
{Rml::PropertyId::Color, rml_color(TextDim)},
{Rml::PropertyId::Cursor, rml_string("pointer")},
{Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto},
{Rml::PropertyId::NavUp, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavDown, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavLeft, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavRight, Rml::Style::Nav::Auto},
{Rml::PropertyId::Opacity, rml_number(1.0f)},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
});
add_focus_border(m_element, BorderRadiusSmall);
auto* left = append(m_element, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::RowGap, rml_dp(4.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(4.0f)},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::Width, rml_px(0.0f)},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::FlexShrink, rml_number(1.0f)},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
});
m_title = add_label(left, title, LabelStyle::Large);
set_props(m_title, {
{Rml::PropertyId::Color, rml_color(TextDim)},
{Rml::PropertyId::FontSize, rml_dp(28.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(1.0f)},
});
if (!value.empty() || !detail.empty()) {
auto* right = append(m_element, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::FlexEnd},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::RowGap, rml_dp(4.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(4.0f)},
{Rml::PropertyId::MinWidth, rml_dp(170.0f)},
{Rml::PropertyId::MaxWidth, rml_percent(48.0f)},
{Rml::PropertyId::FlexShrink, rml_number(0.0f)},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
});
if (!value.empty()) {
m_value = add_label(right, value, LabelStyle::Body);
set_props(
m_value, {
{Rml::PropertyId::Color, rml_color(TextDim)},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Right},
{Rml::PropertyId::OverflowX, Rml::Style::Overflow::Hidden},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Hidden},
{Rml::PropertyId::TextOverflow, Rml::Style::TextOverflow::Ellipsis},
{Rml::PropertyId::WhiteSpace, Rml::Style::WhiteSpace::Nowrap},
});
}
if (!detail.empty()) {
m_detail = add_label(right, detail, LabelStyle::Annotation);
set_props(m_detail, {
{Rml::PropertyId::Color, rml_color(TextDim)},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::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;
apply_control_surface_style(
m_element, control_surface_style(ControlSurfaceTone::Quiet), active);
m_element->SetProperty(
Rml::PropertyId::Color, rml_color(active ? theme::TextActive : theme::TextDim));
m_title->SetProperty(
Rml::PropertyId::Color, rml_color(active ? theme::TextActive : theme::TextDim));
if (m_value != nullptr) {
m_value->SetProperty(
Rml::PropertyId::Color, rml_color(active ? theme::TextActive : theme::TextDim));
}
if (m_detail != nullptr) {
m_detail->SetProperty(Rml::PropertyId::Color, rml_color(theme::TextDim));
}
set_focus_border_visible(m_element, m_focused);
}
} // namespace dusk::ui
-42
View File
@@ -1,42 +0,0 @@
#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
-53
View File
@@ -1,53 +0,0 @@
#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, {
{Rml::PropertyId::FontSize, rml_dp(18.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(2.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Normal},
{Rml::PropertyId::Color, rml_color(TextDim)},
});
break;
case LabelStyle::Body:
set_props(element, {
{Rml::PropertyId::FontSize, rml_dp(20.0f)},
{Rml::PropertyId::LetterSpacing, rml_px(0.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Normal},
{Rml::PropertyId::Color, rml_color(Text)},
});
break;
case LabelStyle::Medium:
set_props(element, {
{Rml::PropertyId::FontSize, rml_dp(28.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(3.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Bold},
{Rml::PropertyId::Color, rml_color(Text)},
});
break;
case LabelStyle::Large:
set_props(element, {
{Rml::PropertyId::FontSize, rml_dp(36.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(4.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Bold},
{Rml::PropertyId::Color, rml_color(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
@@ -1,19 +0,0 @@
#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
-138
View File
@@ -1,138 +0,0 @@
#include "prelaunch_layout.hpp"
#include "element.hpp"
#include "label.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
namespace dusk::ui::prelaunch::layout {
void style_document(Rml::ElementDocument* document) {
using namespace theme;
set_props(document, {
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::Height, rml_percent(100.0f)},
{Rml::PropertyId::MarginTop, rml_px(0.0f)},
{Rml::PropertyId::MarginRight, rml_px(0.0f)},
{Rml::PropertyId::MarginBottom, rml_px(0.0f)},
{Rml::PropertyId::MarginLeft, rml_px(0.0f)},
{Rml::PropertyId::PaddingTop, rml_px(0.0f)},
{Rml::PropertyId::PaddingRight, rml_px(0.0f)},
{Rml::PropertyId::PaddingBottom, rml_px(0.0f)},
{Rml::PropertyId::PaddingLeft, rml_px(0.0f)},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
{Rml::PropertyId::BackgroundColor, rml_color(Background1)},
{Rml::PropertyId::Color, rml_color(Text)},
});
}
Rml::Element* add_screen(Rml::ElementDocument* document, ScreenLayout layout) {
using namespace theme;
const bool compact = layout == ScreenLayout::CompactSplit;
return append(document, "div", "prelaunch-screen",
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Absolute},
{Rml::PropertyId::Left, rml_px(0.0f)},
{Rml::PropertyId::Top, rml_px(0.0f)},
{Rml::PropertyId::Right, rml_px(0.0f)},
{Rml::PropertyId::Bottom, rml_px(0.0f)},
{Rml::PropertyId::FlexDirection,
compact ? Rml::Style::FlexDirection::Row : Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::RowGap, rml_dp(compact ? 28.0f : 24.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(compact ? 28.0f : 24.0f)},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::PaddingTop, rml_dp(compact ? 24.0f : 48.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(compact ? 24.0f : 28.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(compact ? 24.0f : 48.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(compact ? 24.0f : 28.0f)},
{Rml::PropertyId::BackgroundColor, rml_color(Background1)},
});
}
Rml::Element* add_brand(Rml::Element* parent, std::string_view logoPath, bool compact) {
auto* brand = append(parent, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::RowGap, rml_dp(compact ? 8.0f : 12.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(compact ? 8.0f : 12.0f)},
{Rml::PropertyId::Width, compact ? rml_dp(260.0f) : rml_percent(100.0f)},
{Rml::PropertyId::MaxWidth, compact ? rml_percent(32.0f) : rml_dp(720.0f)},
{Rml::PropertyId::FlexShrink, rml_number(compact ? 0.0f : 1.0f)},
});
auto* subtitle = add_label(brand, "Twilit Realm presents", LabelStyle::Annotation);
set_props(subtitle, {
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Center},
{Rml::PropertyId::FontSize, rml_dp(compact ? 14.0f : 18.0f)},
});
if (!logoPath.empty()) {
auto* logo = append(brand, "img");
logo->SetAttribute("src", std::string(logoPath));
set_props(logo, {
{Rml::PropertyId::Width, rml_dp(compact ? 220.0f : 360.0f)},
{Rml::PropertyId::MaxWidth, rml_percent(compact ? 100.0f : 70.0f)},
{Rml::PropertyId::Height, Rml::Style::Height::Auto},
});
} else {
auto* title = add_label(brand, "Dusk", LabelStyle::Large);
set_props(title, {
{Rml::PropertyId::FontSize, rml_dp(compact ? 42.0f : 54.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(compact ? 3.0f : 4.0f)},
});
}
return brand;
}
Rml::Element* add_heading(Rml::Element* parent, std::string_view title) {
auto* heading = add_label(parent, title, LabelStyle::Large);
set_props(heading, {
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::MaxWidth, rml_dp(840.0f)},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Left},
});
return heading;
}
Rml::Element* add_panel(Rml::Element* parent, bool wide, bool compact) {
using namespace theme;
return append(parent, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::RowGap, rml_dp(12.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(12.0f)},
{Rml::PropertyId::Width, rml_dp(wide ? 840.0f : 520.0f)},
{Rml::PropertyId::MaxWidth, rml_percent(compact ? 62.0f : 100.0f)},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::PaddingTop, rml_dp(compact ? 16.0f : 20.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(compact ? 16.0f : 20.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(compact ? 16.0f : 20.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(compact ? 16.0f : 20.0f)},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderTopColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderRightColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderBottomColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderLeftColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BackgroundColor, rml_color(ElevatedSoft)},
});
}
} // namespace dusk::ui::prelaunch::layout
-22
View File
@@ -1,22 +0,0 @@
#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
-933
View File
@@ -1,933 +0,0 @@
#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 "Disc is for a different game";
case iso::ValidationError::WrongVersion:
return "Disc is for an unsupported version. Only NTSC & PAL GameCube are "
"supported at this time";
case iso::ValidationError::ExecutableMismatch:
return "Disc contains modified executable files";
case iso::ValidationError::Success:
return {};
case iso::ValidationError::Unknown:
default:
return "Unknown disc 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 selected_disc_text() const {
if (!m_selectedIsoPath.empty()) {
return display_path(m_selectedIsoPath);
}
return "Select a disc...";
}
std::string disc_status_text() const {
if (!m_errorString.empty()) {
return m_errorString;
}
if (is_selected_path_valid()) {
return fmt::format("Disc region: {}", m_isPal ? "PAL" : "NTSC");
}
return {};
}
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, selected_disc_text(), disc_status_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
@@ -1,13 +0,0 @@
#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
-3
View File
@@ -1,3 +0,0 @@
#include "theme.hpp"
namespace dusk::ui::theme {} // namespace dusk::ui::theme
-71
View File
@@ -1,71 +0,0 @@
#pragma once
#include <RmlUi/Core/Property.h>
#include <string>
namespace dusk::ui {
namespace 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 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 Color WindowSurface{21, 22, 16, 178};
inline constexpr Color WindowTitleOverlay{217, 217, 217, 26};
inline constexpr Color WindowDivider{146, 135, 91, 255};
inline constexpr Color WindowAccent{194, 164, 45, 255};
inline constexpr Color WindowAccentSoft{204, 184, 119, 255};
inline constexpr Color WindowItemSurface{17, 16, 10, 128};
inline constexpr Color WindowGlyph{224, 219, 200, 255};
inline constexpr float WindowTabBarHeight = 66.0f;
inline constexpr float BorderRadiusSmall = 8.0f;
inline constexpr float BorderRadiusMedium = 12.0f;
inline constexpr float BorderWidth = 2.0f;
} // namespace theme
static Rml::Property rml_color(theme::Color color, int opacity = -1) {
if (opacity >= 0) {
color.a = std::clamp(opacity, 0, 255);
}
return Rml::Property(Rml::Colourb(color.r, color.g, color.b, color.a), Rml::Unit::COLOUR);
}
static Rml::Property rml_dp(float value) {
return Rml::Property(value, Rml::Unit::DP);
}
static Rml::Property rml_percent(float value) {
return Rml::Property(value, Rml::Unit::PERCENT);
}
static Rml::Property rml_px(float value) {
return Rml::Property(value, Rml::Unit::PX);
}
static Rml::Property rml_number(float value) {
return Rml::Property(value, Rml::Unit::NUMBER);
}
static Rml::Property rml_string(Rml::String value) {
return Rml::Property(std::move(value), Rml::Unit::STRING);
}
} // namespace dusk::ui
+16 -204
View File
@@ -1,109 +1,23 @@
#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() {
static bool s_initialized = false;
bool initialize() noexcept {
if (s_initialized) {
return true;
}
@@ -111,134 +25,32 @@ bool initialize() {
return false;
}
load_font("Inter-Regular.ttf");
load_font("Inter-Bold.ttf");
load_font("NotoMono-Regular.ttf", true);
load_font("FiraSans-Regular.ttf", true);
load_font("FiraSansCondensed-Regular.ttf");
load_font("FiraSansCondensed-Bold.ttf");
s_initialized = true;
return true;
}
void shutdown() {
s_active = false;
void shutdown() noexcept {
s_initialized = false;
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
bool is_active() {
return s_active;
void handle_event(const SDL_Event& event) noexcept {
// TODO
}
void set_active(bool active) {
s_active = active;
if (!active) {
sLatestControllerKey = Rml::Input::KI_UNKNOWN;
}
void update() noexcept {
// TODO
}
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;
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
const char* basePath = SDL_GetBasePath();
if (basePath == nullptr) {
return std::filesystem::path("res") / filename;
}
return std::filesystem::path(basePath) / "res" / filename;
}
} // namespace dusk::ui
+7 -19
View File
@@ -1,29 +1,17 @@
#pragma once
#include <RmlUi/Core/Input.h>
#include <SDL3/SDL_events.h>
#include <filesystem>
namespace dusk::ui {
enum class MenuAction {
None,
Accept,
Back,
Toggle,
TabLeft,
TabRight,
};
bool initialize() noexcept;
void shutdown() noexcept;
bool initialize();
void shutdown();
void handle_event(const SDL_Event& event) noexcept;
void update() noexcept;
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);
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
} // namespace dusk::ui
+232 -427
View File
@@ -1,464 +1,269 @@
#include "window.hpp"
#include "element.hpp"
#include "focus_border.hpp"
#include "theme.hpp"
#include <RmlUi/Core.h>
#include <RmlUi/Core/PropertyDictionary.h>
#include <RmlUi/Core/StyleSheetSpecification.h>
#include <algorithm>
#include "aurora/rmlui.hpp"
namespace dusk::ui {
namespace {
WindowTab::WindowTab(Rml::Element* parent, std::string_view id, std::string_view label,
std::function<void()> selectedCallback)
: m_selectedCallback(std::move(selectedCallback)) {
using namespace theme;
m_element = append(parent, "button", id,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Height, rml_percent(100.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(20.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(20.0f)},
{Rml::PropertyId::BackgroundColor, rml_color(Transparent)},
{Rml::PropertyId::BorderTopWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderRightWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderBottomWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderLeftWidth, rml_px(0.0f)},
{Rml::PropertyId::Cursor, rml_string("pointer")},
{Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto},
{Rml::PropertyId::NavUp, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavDown, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavLeft, Rml::Style::Nav::Auto},
{Rml::PropertyId::NavRight, Rml::Style::Nav::Auto},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
});
add_focus_border(m_element, BorderRadiusSmall);
m_label = append(m_element, "span", {},
{
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
{Rml::PropertyId::FontSize, rml_dp(20.0f)},
{Rml::PropertyId::LetterSpacing, rml_dp(1.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Bold},
{Rml::PropertyId::TextAlign, Rml::Style::TextAlign::Center},
{Rml::PropertyId::Color, rml_color(Text)},
});
set_text(m_label, label);
m_indicator = append(m_element, "div", {},
{
{Rml::PropertyId::Position, Rml::Style::Position::Absolute},
{Rml::PropertyId::Left, rml_px(0.0f)},
{Rml::PropertyId::Right, rml_px(0.0f)},
{Rml::PropertyId::Bottom, rml_dp(-BorderWidth)},
{Rml::PropertyId::Height, rml_dp(2.0f)},
{Rml::PropertyId::BackgroundColor, rml_color(WindowAccent, 0)},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::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();
}
WindowTab::~WindowTab() {
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 WindowTab::ProcessEvent(Rml::Event& event) {
switch (event.GetId()) {
case Rml::EventId::Click:
if (m_selectedCallback) {
m_selectedCallback();
const Rml::String kWindowDocumentRml = R"RML(
<rml>
<head>
<title>Window</title>
<style>
*, *:before, *:after {
box-sizing: border-box;
}
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;
}
}
std::string WindowTab::id() const {
return m_element == nullptr ? std::string{} : m_element->GetId();
}
body {
width: 100%;
height: 100%;
padding: 64dp;
font-family: "Fira Sans";
font-weight: normal;
font-style: normal;
font-size: 15dp;
color: #E0DBC8;
}
void WindowTab::set_selected(bool selected) {
if (m_selected == selected) {
return;
}
m_selected = selected;
apply_style();
}
.window {
max-width: 1088dp;
max-height: 768dp;
margin: auto;
border-radius: 14dp;
overflow: hidden;
border: 2dp #92875B;
backdrop-filter: blur(5dp);
box-shadow: 0 0 25dp 5dp;
background-color: rgba(21, 22, 16, 90%);
}
void WindowTab::apply_style() {
using namespace theme;
.window .tab-bar {
display: flex;
height: 64dp;
background-color: rgba(217, 217, 217, 10%);
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 18dp;
text-transform: uppercase;
border-bottom: 2dp #92875B;
}
if (m_element == nullptr) {
return;
.window .tab-bar .tab {
padding: 0 24dp;
line-height: 64dp;
opacity: 0.25;
tab-index: auto;
nav: horizontal;
focus: auto;
}
.window .tab-bar .tab.active {
opacity: 1;
border-bottom: 4dp #C2A42D;
font-effect: glow(0dp 4dp 0dp 4dp black);
decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%);
}
.window .tab-bar .tab:focus-visible {
opacity: 1;
font-effect: glow(0dp 4dp 0dp 4dp black);
decorator: linear-gradient(to bottom, rgba(194, 164, 45, 0%) 0%, rgba(194, 164, 45, 15%) 100%);
}
.window .content {
display: flex;
height: 100%;
}
.window .content .pane {
display: flex;
flex-flow: column;
flex: 1 1 0;
height: 100%;
padding: 24dp;
gap: 8dp;
overflow: auto;
}
.window .content .pane:not(:last-of-type) {
border-right: 1dp #92875B;
}
.section-heading {
font-weight: bold;
text-transform: uppercase;
font-size: 22dp;
opacity: 0.25;
}
.button {
text-align: center;
background-color: rgba(17, 16, 10, 20%);
opacity: 0.9;
padding: 8dp 16dp;
border-radius: 14dp;
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
font-size: 20dp;
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
}
.button.active, .button:hover {
background-color: rgba(204, 184, 119, 20%);
box-shadow: #C2A42D 0 0 0 2dp;
}
.button.selected, .button:active {
opacity: 1;
background-color: rgba(204, 184, 119, 40%);
box-shadow: #C2A42D 0 0 0 2dp;
}
.select-button {
display: flex;
align-items: center;
gap: 8dp;
background-color: rgba(17, 16, 10, 20%);
opacity: 0.9;
padding: 8dp 16dp;
border-radius: 14dp;
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
}
.select-button.active, .select-button:hover {
background-color: rgba(204, 184, 119, 20%);
box-shadow: #C2A42D 0 0 0 2dp;
}
.select-button.selected, .select-button:active {
opacity: 1;
background-color: rgba(204, 184, 119, 40%);
box-shadow: #C2A42D 0 0 0 2dp;
}
.select-button .key {
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 18dp;
text-transform: uppercase;
}
.select-button .value {
margin-left: auto;
font-size: 20dp;
}
</style>
</head>
<body data-model="window">
<div class="window">
<div class="tab-bar">
<button class="tab"
data-for="tab, i : tabs"
data-class-active="i == active_tab"
data-event-click="set_active_tab(i)">
{{ tab.label }}
</button>
</div>
<div id="content" class="content"></div>
</div>
</body>
</rml>
)RML";
bool setup_window_model(Rml::Context* context, WindowModel& model, Rml::DataModelHandle& handle) {
Rml::DataModelConstructor constructor = context->CreateDataModel("window");
if (!constructor) {
return false;
}
const bool active = m_hovered || m_focused;
int textOpacity;
if (m_selected) {
textOpacity = 255;
} else if (active) {
textOpacity = 200;
if (auto tab_handle = constructor.RegisterStruct<WindowTab>()) {
tab_handle.RegisterMember("label", &WindowTab::label);
} else {
textOpacity = 110;
}
const Color textColor = m_selected ? TextActive : Text;
m_label->SetProperty(Rml::PropertyId::Color, rml_color(textColor, textOpacity));
if (m_indicator != nullptr) {
const int indicatorOpacity = m_selected ? 255 : (active ? 96 : 0);
m_indicator->SetProperty(
Rml::PropertyId::BackgroundColor, rml_color(WindowAccent, indicatorOpacity));
return false;
}
set_focus_border_visible(m_element, m_focused);
if (!constructor.RegisterArray<std::vector<WindowTab> >()) {
return false;
}
constructor.Bind("active_tab", &model.activeTab);
constructor.Bind("tabs", &model.tabs);
constructor.BindEventCallback("set_active_tab", &WindowModel::set_active_tab, &model);
handle = constructor.GetModelHandle();
return true;
}
Window::Window(Rml::Element* parent, std::string_view id, std::function<void()> closeCallback)
: m_closeCallback(std::move(closeCallback)) {
using namespace theme;
} // namespace
m_element = append(parent, "div", id,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::MaxWidth, rml_dp(1088.0f)},
{Rml::PropertyId::BorderTopWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderRightWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderLeftWidth, rml_dp(BorderWidth)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusMedium)},
{Rml::PropertyId::BorderTopColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderRightColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderBottomColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BorderLeftColor, rml_color(ElevatedBorder)},
{Rml::PropertyId::BackgroundColor, rml_color(WindowSurface)},
{Rml::PropertyId::OverflowX, Rml::Style::Overflow::Hidden},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Hidden},
});
set_props(m_element, {
{"backdrop-filter", "blur(5dp)"},
{"box-shadow", "0 0 25dp 5dp"},
});
void WindowModel::set_active_tab(
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
if (arguments.empty()) {
return;
}
m_tabBar = append(m_element, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::Height, rml_dp(WindowTabBarHeight)},
{Rml::PropertyId::MinHeight, rml_dp(WindowTabBarHeight)},
{Rml::PropertyId::PaddingLeft, rml_dp(12.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(12.0f)},
{Rml::PropertyId::RowGap, rml_dp(4.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(4.0f)},
{Rml::PropertyId::BackgroundColor, rml_color(WindowTitleOverlay)},
{Rml::PropertyId::BorderBottomWidth, rml_dp(BorderWidth * 1.5f)},
{Rml::PropertyId::BorderBottomColor, rml_color(WindowDivider)},
});
const int tabIndex = arguments[0].Get<int>();
if (tabIndex < 0 || tabIndex >= static_cast<int>(tabs.size()) || tabIndex == activeTab) {
return;
}
m_tabStrip = append(m_tabBar, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Stretch},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::FlexStart},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::FlexShrink, rml_number(1.0f)},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::Height, rml_percent(100.0f)},
{Rml::PropertyId::RowGap, rml_dp(4.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(4.0f)},
});
activeTab = tabIndex;
model.DirtyVariable("active_tab");
const std::string closeId = id.empty() ? std::string{} : std::string(id) + "-close";
m_closeButton = append(m_tabBar, "button", closeId,
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::Position, Rml::Style::Position::Relative},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Center},
{Rml::PropertyId::JustifyContent, Rml::Style::JustifyContent::Center},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Width, rml_dp(36.0f)},
{Rml::PropertyId::Height, rml_dp(36.0f)},
{Rml::PropertyId::FlexShrink, rml_number(0.0f)},
{Rml::PropertyId::BorderTopWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderRightWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderBottomWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderLeftWidth, rml_px(0.0f)},
{Rml::PropertyId::BorderTopLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderTopRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomRightRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BorderBottomLeftRadius, rml_dp(BorderRadiusSmall)},
{Rml::PropertyId::BackgroundColor, rml_color(Transparent)},
{Rml::PropertyId::Cursor, rml_string("pointer")},
{Rml::PropertyId::TabIndex, Rml::Style::TabIndex::Auto},
{Rml::PropertyId::FontFamily, rml_string("Inter")},
});
add_focus_border(m_closeButton, BorderRadiusSmall);
// Replace window content with new tab content
auto* currentElem = event.GetCurrentElement();
if (currentElem == nullptr) {
return;
}
auto* doc = currentElem->GetOwnerDocument();
if (doc == nullptr) {
return;
}
auto* content = doc->GetElementById("content");
if (content == nullptr) {
return;
}
while (content->GetNumChildren() > 0) {
content->RemoveChild(content->GetFirstChild());
}
if (tabs[tabIndex].setContent) {
tabs[tabIndex].setContent(content);
}
}
auto* closeGlyph = append(m_closeButton, "span", {},
{
{Rml::PropertyId::FontSize, rml_dp(22.0f)},
{Rml::PropertyId::FontWeight, Rml::Style::FontWeight::Normal},
{Rml::PropertyId::Color, rml_color(WindowGlyph)},
{Rml::PropertyId::PointerEvents, Rml::Style::PointerEvents::None},
});
set_text(closeGlyph, "\xc3\x97");
m_closeButton->AddEventListener(Rml::EventId::Click, this);
m_closeButton->AddEventListener(Rml::EventId::Focus, this);
m_closeButton->AddEventListener(Rml::EventId::Blur, this);
m_closeButton->AddEventListener(Rml::EventId::Mouseover, this);
m_closeButton->AddEventListener(Rml::EventId::Mouseout, this);
m_contentRow = append(m_element, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Row},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::Stretch},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::Width, rml_percent(100.0f)},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::FlexShrink, rml_number(1.0f)},
{Rml::PropertyId::MinHeight, rml_px(0.0f)},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::RowGap, rml_dp(20.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(20.0f)},
});
m_leftPane = append(m_contentRow, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::MinHeight, rml_px(0.0f)},
{Rml::PropertyId::PaddingTop, rml_dp(24.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(24.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(24.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(24.0f)},
{Rml::PropertyId::RowGap, rml_dp(12.0f)},
{Rml::PropertyId::ColumnGap, rml_dp(12.0f)},
});
m_rightPane = append(m_contentRow, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::None},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::MinHeight, rml_px(0.0f)},
{Rml::PropertyId::PaddingTop, rml_dp(24.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(24.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(24.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(8.0f)},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::FlexStart},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Auto},
});
m_rightPane = append(m_contentRow, "div", {},
{
{Rml::PropertyId::Display, Rml::Style::Display::None},
{Rml::PropertyId::FlexDirection, Rml::Style::FlexDirection::Column},
{Rml::PropertyId::BoxSizing, Rml::Style::BoxSizing::BorderBox},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
{Rml::PropertyId::MinHeight, rml_px(0.0f)},
{Rml::PropertyId::PaddingTop, rml_dp(24.0f)},
{Rml::PropertyId::PaddingBottom, rml_dp(24.0f)},
{Rml::PropertyId::PaddingRight, rml_dp(24.0f)},
{Rml::PropertyId::PaddingLeft, rml_dp(8.0f)},
{Rml::PropertyId::AlignItems, Rml::Style::AlignItems::FlexStart},
{Rml::PropertyId::OverflowY, Rml::Style::Overflow::Auto},
});
apply_pane_layout();
apply_close_style();
Window::Window(WindowModel model) : mModel(std::move(model)) {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
setup_window_model(context, mModel, mModelHandle);
mDocument = context->LoadDocumentFromMemory(kWindowDocumentRml);
if (mDocument == nullptr) {
return;
}
mModel.tabs[0].setContent(mDocument->GetElementById("content"));
}
Window::~Window() {
m_tabs.clear();
if (m_closeButton != nullptr) {
m_closeButton->RemoveEventListener(Rml::EventId::Click, this);
m_closeButton->RemoveEventListener(Rml::EventId::Focus, this);
m_closeButton->RemoveEventListener(Rml::EventId::Blur, this);
m_closeButton->RemoveEventListener(Rml::EventId::Mouseover, this);
m_closeButton->RemoveEventListener(Rml::EventId::Mouseout, this);
m_closeButton = nullptr;
}
m_element = nullptr;
}
void Window::ProcessEvent(Rml::Event& event) {
if (event.GetTargetElement() != m_closeButton) {
return;
}
switch (event.GetId()) {
case Rml::EventId::Click:
if (m_closeCallback) {
m_closeCallback();
}
break;
case Rml::EventId::Focus:
m_closeFocused = true;
apply_close_style();
break;
case Rml::EventId::Blur:
m_closeFocused = false;
apply_close_style();
break;
case Rml::EventId::Mouseover:
m_closeHovered = true;
apply_close_style();
break;
case Rml::EventId::Mouseout:
m_closeHovered = false;
apply_close_style();
break;
default:
break;
auto* context = aurora::rmlui::get_context();
if (context != nullptr && mDocument != nullptr) {
context->UnloadDocument(mDocument);
mDocument = nullptr;
}
}
WindowTab* Window::add_tab(
std::string_view id, std::string_view label, std::function<void()> selectedCallback) {
if (m_tabStrip == nullptr) {
return nullptr;
}
const std::string idString(id);
auto wrapped = [this, idString, cb = std::move(selectedCallback)]() {
set_selected_tab(idString);
if (cb) {
cb();
}
};
auto tab = std::make_unique<WindowTab>(m_tabStrip, idString, label, std::move(wrapped));
WindowTab* raw = tab.get();
m_tabs.push_back(std::move(tab));
return raw;
}
void Window::set_selected_tab(std::string_view id) {
for (auto& tab : m_tabs) {
tab->set_selected(tab->id() == id);
void Window::show() {
if (mDocument != nullptr) {
mDocument->Show();
}
}
std::string Window::selected_tab_id() const {
for (const auto& tab : m_tabs) {
if (tab->is_selected()) {
return tab->id();
}
}
return {};
}
void Window::set_right_pane_visible(bool visible) {
if (m_rightPaneVisible == visible) {
return;
}
m_rightPaneVisible = visible;
apply_pane_layout();
}
void Window::apply_pane_layout() {
using namespace theme;
if (m_leftPane == nullptr || m_rightPane == nullptr) {
return;
}
if (m_rightPaneVisible) {
set_props(m_leftPane, {
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::FlexShrink, rml_number(1.0f)},
{Rml::PropertyId::FlexBasis, rml_px(0.0f)},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
});
set_props(m_rightPane, {
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexGrow, rml_number(0.0f)},
{Rml::PropertyId::FlexShrink, rml_number(0.0f)},
{Rml::PropertyId::FlexBasis, rml_percent(40.0f)},
{Rml::PropertyId::MinWidth, rml_px(0.0f)},
});
} else {
set_props(
m_leftPane, {
{Rml::PropertyId::Display, Rml::Style::Display::Flex},
{Rml::PropertyId::FlexGrow, rml_number(1.0f)},
{Rml::PropertyId::FlexShrink, rml_number(1.0f)},
{Rml::PropertyId::FlexBasis, Rml::Style::LengthPercentageAuto::Auto},
{Rml::PropertyId::Width, rml_percent(100.0f)},
});
set_props(m_rightPane, {
{Rml::PropertyId::Display, Rml::Style::Display::None},
});
void Window::hide() {
if (mDocument != nullptr) {
mDocument->Hide();
}
}
void Window::apply_close_style() {
using namespace theme;
if (m_closeButton == nullptr) {
return;
}
const bool active = m_closeHovered || m_closeFocused;
m_closeButton->SetProperty(Rml::PropertyId::BackgroundColor,
active ? rml_color(WindowAccent, 56) : rml_color(Transparent));
set_focus_border_visible(m_closeButton, m_closeFocused);
}
} // namespace dusk::ui
+23 -72
View File
@@ -1,84 +1,35 @@
#pragma once
#include <RmlUi/Core/EventListener.h>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace Rml {
class Element;
}
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/ElementDocument.h>
namespace dusk::ui {
class WindowTab : public Rml::EventListener {
public:
WindowTab(Rml::Element* parent, std::string_view id, std::string_view label,
std::function<void()> selectedCallback);
~WindowTab() override;
WindowTab(const WindowTab&) = delete;
WindowTab& operator=(const WindowTab&) = delete;
void ProcessEvent(Rml::Event& event) override;
Rml::Element* element() const { return m_element; }
std::string id() const;
void set_selected(bool selected);
bool is_selected() const { return m_selected; }
private:
Rml::Element* m_element = nullptr;
Rml::Element* m_label = nullptr;
Rml::Element* m_indicator = nullptr;
std::function<void()> m_selectedCallback;
bool m_hovered = false;
bool m_focused = false;
bool m_selected = false;
void apply_style();
struct WindowTab {
Rml::String label;
std::function<void(Rml::Element*)> setContent;
};
class Window : public Rml::EventListener {
struct WindowModel {
int activeTab = 0;
std::vector<WindowTab> tabs;
void set_active_tab(
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
};
class Window {
public:
Window(Rml::Element* parent, std::string_view id, std::function<void()> closeCallback = {});
~Window() override;
Window(WindowModel model);
~Window();
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
void ProcessEvent(Rml::Event& event) override;
Rml::Element* element() const { return m_element; }
Rml::Element* body() const { return m_leftPane; }
Rml::Element* right_pane() const { return m_rightPane; }
void set_right_pane_visible(bool visible);
Rml::Element* tab_strip() const { return m_tabStrip; }
WindowTab* add_tab(
std::string_view id, std::string_view label, std::function<void()> selectedCallback);
void set_selected_tab(std::string_view id);
std::string selected_tab_id() const;
void show();
void hide();
private:
Rml::Element* m_element = nullptr;
Rml::Element* m_tabBar = nullptr;
Rml::Element* m_tabStrip = nullptr;
Rml::Element* m_closeButton = nullptr;
Rml::Element* m_contentRow = nullptr;
Rml::Element* m_leftPane = nullptr;
Rml::Element* m_rightPane = nullptr;
bool m_rightPaneVisible = false;
std::function<void()> m_closeCallback;
std::vector<std::unique_ptr<WindowTab> > m_tabs;
bool m_closeHovered = false;
bool m_closeFocused = false;
void apply_close_style();
void apply_pane_layout();
WindowModel mModel;
Rml::DataModelHandle mModelHandle;
Rml::ElementDocument* mDocument;
};
} // namespace dusk::ui
} // namespace dusk::ui
+28 -19
View File
@@ -56,8 +56,8 @@
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/ui/game_menu.hpp"
#include "dusk/ui/prelaunch_screen.hpp"
#include "dusk/ui/ui.hpp"
#include "dusk/ui/editor.hpp"
#include "version.h"
#include <aurora/aurora.h>
@@ -77,6 +77,7 @@
#include "tracy/Tracy.hpp"
#include "f_pc/f_pc_draw.h"
#include "tracy/Tracy.hpp"
#include <RmlUi/Core.h>
// --- GLOBALS ---
s8 mDoMain::developmentMode = -1;
@@ -135,23 +136,18 @@ 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);
dusk::ui::handle_event(event->sdl);
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
break;
case AURORA_DISPLAY_SCALE_CHANGED:
// dusk::ImGuiEngine_Initialize(event->windowSize.scale);
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
break;
case AURORA_EXIT:
dusk::ui::prelaunch::shutdown();
return false;
}
@@ -163,15 +159,13 @@ bool launchUILoop() {
continue;
}
if (useRmlPrelaunch) {
dusk::ui::prelaunch::update();
}
dusk::g_imguiConsole.PreDraw();
dusk::g_imguiConsole.PostDraw();
aurora_end_frame();
}
dusk::ui::prelaunch::shutdown();
return dusk::IsRunning;
}
@@ -214,7 +208,6 @@ void main01(void) {
OSReport("Entering Main Loop (main01)...\n");
dusk::game_clock::ensure_initialized();
dusk::ui::game_menu::initialize();
do {
// 1. Update Window Events
@@ -224,7 +217,8 @@ void main01(void) {
case AURORA_NONE:
goto eventsDone;
case AURORA_SDL_EVENT:
dusk::ui::game_menu::handle_event(event->sdl);
dusk::ui::handle_event(event->sdl);
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST &&
dusk::getSettings().game.pauseOnFocusLost) {
dusk::IsFocusPaused = true;
@@ -236,6 +230,9 @@ void main01(void) {
dusk::game_clock::reset_frame_timer();
}
break;
case AURORA_DISPLAY_SCALE_CHANGED:
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
break;
case AURORA_EXIT:
goto exit;
}
@@ -260,7 +257,7 @@ void main01(void) {
mDoGph_gInf_c::updateRenderSize();
dusk::ui::game_menu::update();
dusk::ui::update();
const auto pacing = dusk::game_clock::advance_main_loop();
if (pacing.is_interpolating) {
@@ -313,7 +310,7 @@ void main01(void) {
} while (dusk::IsRunning);
exit:;
dusk::ui::game_menu::shutdown();
dusk::ui::shutdown();
}
static bool IsBackendAvailable(AuroraBackend backend) {
@@ -359,6 +356,11 @@ static AuroraBackend ResolveDesiredBackend(const cxxopts::ParseResult& parsedArg
return desiredBackend;
}
static void aurora_imgui_init_callback(const AuroraWindowSize* size) {
dusk::ImGuiEngine_Initialize(size->scale);
dusk::ImGuiEngine_AddTextures();
}
static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
if (option.count() == 0) {
return;
@@ -559,6 +561,7 @@ int game_main(int argc, char* argv[]) {
config.mem1Size = 256 * 1024 * 1024;
config.mem2Size = 24 * 1024 * 1024;
config.allowJoystickBackgroundEvents = true;
config.imGuiInitCallback = &aurora_imgui_init_callback;
config.allowTextureReplacements = true;
config.allowTextureDumps = false;
auroraInfo = aurora_initialize(argc, argv, &config);
@@ -581,6 +584,11 @@ int game_main(int argc, char* argv[]) {
dusk::audio::SetMasterVolume(dusk::getSettings().audio.masterVolume / 100.0f);
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
dusk::ui::initialize();
// TODO: just for testing
dusk::ui::EditorWindow editorWindow;
editorWindow.show();
std::string dvd_path;
bool dvd_opened = false;
@@ -659,6 +667,7 @@ int game_main(int argc, char* argv[]) {
#ifdef DUSK_DISCORD_RPC
dusk::discord::Shutdown();
#endif
dusk::ui::shutdown();
aurora_shutdown();
return 0;