mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-07 10:11:43 -04:00
Create new component system & initial settings window
This commit is contained in:
+12
@@ -1462,8 +1462,20 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiStateShare.cpp
|
||||
src/dusk/imgui/ImGuiAchievements.hpp
|
||||
src/dusk/imgui/ImGuiAchievements.cpp
|
||||
src/dusk/ui/button.cpp
|
||||
src/dusk/ui/button.hpp
|
||||
src/dusk/ui/component.cpp
|
||||
src/dusk/ui/component.hpp
|
||||
src/dusk/ui/editor.cpp
|
||||
src/dusk/ui/editor.hpp
|
||||
src/dusk/ui/event.cpp
|
||||
src/dusk/ui/event.hpp
|
||||
src/dusk/ui/pane.cpp
|
||||
src/dusk/ui/pane.hpp
|
||||
src/dusk/ui/select_button.cpp
|
||||
src/dusk/ui/select_button.hpp
|
||||
src/dusk/ui/settings.cpp
|
||||
src/dusk/ui/settings.hpp
|
||||
src/dusk/ui/ui.hpp
|
||||
src/dusk/ui/ui.cpp
|
||||
src/dusk/ui/window.hpp
|
||||
|
||||
@@ -45,7 +45,7 @@ body {
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
.window .tab-bar .tab.active {
|
||||
.window .tab-bar .tab.selected {
|
||||
opacity: 1;
|
||||
border-bottom: 4dp #C2A42D;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
+3
-10
@@ -3,16 +3,9 @@
|
||||
<title>Window</title>
|
||||
<link type="text/rcss" href="window.rcss" />
|
||||
</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>
|
||||
<body>
|
||||
<div id="window" class="window">
|
||||
<div id="tab-bar" class="tab-bar"></div>
|
||||
<div id="content" class="content"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent, const Rml::String& className) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("button");
|
||||
elem->SetClass(className, true);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Button::Button(Rml::Element* parent, ButtonProps props, const Rml::String& className)
|
||||
: Component(createRoot(parent, className)) {
|
||||
update_props(std::move(props));
|
||||
listen(mRoot, Rml::EventId::Click, [this](Rml::Event& event) {
|
||||
if (mProps.onPressed) {
|
||||
mProps.onPressed(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Button::set_text(const Rml::String& text) {
|
||||
if (mProps.text != text) {
|
||||
mRoot->SetInnerRML(escape(text));
|
||||
mProps.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
void Button::set_selected(bool selected) {
|
||||
if (mProps.selected != selected) {
|
||||
mRoot->SetClass("selected", selected);
|
||||
mProps.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
void Button::update_props(Props props) {
|
||||
set_text(props.text);
|
||||
set_selected(props.selected);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
struct ButtonProps {
|
||||
Rml::String text;
|
||||
std::function<void(Rml::Event&)> onPressed;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
class Button : public Component {
|
||||
public:
|
||||
using Props = ButtonProps;
|
||||
|
||||
Button(Rml::Element* parent, ButtonProps props, const Rml::String& className = "button");
|
||||
|
||||
void set_text(const Rml::String& text);
|
||||
void set_selected(bool selected);
|
||||
|
||||
const Rml::String& get_text() const { return mProps.text; }
|
||||
|
||||
private:
|
||||
void update_props(Props props);
|
||||
|
||||
ButtonProps mProps;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "component.hpp"
|
||||
|
||||
#include "aurora/lib/dolphin/gd/gd.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
static aurora::Module Log{"dusk::ui::component"};
|
||||
|
||||
Component::Component(Rml::Element* root) : mRoot(root) {}
|
||||
|
||||
Component::~Component() = default;
|
||||
|
||||
void Component::update() {
|
||||
for (const auto& child : mChildren) {
|
||||
child->update();
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
|
||||
void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mRoot;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
void Component::clear_children() {
|
||||
mChildren.clear();
|
||||
while (mRoot->GetNumChildren() > 0) {
|
||||
mRoot->RemoveChild(mRoot->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "event.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Rml {
|
||||
class Element;
|
||||
}
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Component {
|
||||
public:
|
||||
Component() = default;
|
||||
explicit Component(Rml::Element* root);
|
||||
virtual ~Component();
|
||||
|
||||
Component(const Component&) = delete;
|
||||
Component& operator=(const Component&) = delete;
|
||||
|
||||
virtual void update();
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mChildren.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component> > mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+88
-55
@@ -4,32 +4,14 @@
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include "aurora/lib/dolphin/gd/gd.hpp"
|
||||
#include "button.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
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";
|
||||
aurora::Module Log{"dusk::ui::editor"};
|
||||
|
||||
bool has_save_data() {
|
||||
return dComIfGs_getSaveData() != nullptr;
|
||||
@@ -44,14 +26,14 @@ dSv_player_status_a_c* get_player_status() {
|
||||
|
||||
Rml::String get_player_name() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
return "Link";
|
||||
}
|
||||
return dComIfGs_getPlayerName();
|
||||
}
|
||||
|
||||
Rml::String get_horse_name() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
return "Epona";
|
||||
}
|
||||
return dComIfGs_getHorseName();
|
||||
}
|
||||
@@ -79,19 +61,17 @@ Rml::String value_for_player_selection(const Rml::String& selection) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
Rml::String make_select_row(std::string_view key, std::string_view label, const Rml::String& value, const Rml::String& activeSelection) {
|
||||
Rml::String make_select_row(std::string_view key, std::string_view label, const Rml::String& value,
|
||||
const Rml::String& activeSelection) {
|
||||
const char* selectedClass = key == activeSelection ? " selected" : "";
|
||||
return fmt::format(
|
||||
"<button class=\"select-button{0}\" data-event-click=\"set_active_selection('{1}')\">"
|
||||
"<div class=\"key\">{2}</div><div class=\"value\">{3}</div></button>",
|
||||
selectedClass,
|
||||
key,
|
||||
label,
|
||||
value
|
||||
);
|
||||
selectedClass, key, label, value);
|
||||
}
|
||||
|
||||
Rml::String make_numeric_detail(std::string_view label, std::string_view decAction, std::string_view incAction) {
|
||||
Rml::String make_numeric_detail(
|
||||
std::string_view label, std::string_view decAction, std::string_view incAction) {
|
||||
return fmt::format(
|
||||
"<div class=\"pane detail-pane\">"
|
||||
"<div class=\"section-heading\">{0}</div>"
|
||||
@@ -100,15 +80,13 @@ Rml::String make_numeric_detail(std::string_view label, std::string_view decActi
|
||||
"<button class=\"button\" data-event-click=\"window_action('{2}')\">+1</button>"
|
||||
"</div>"
|
||||
"</div>",
|
||||
label,
|
||||
decAction,
|
||||
incAction
|
||||
);
|
||||
label, decAction, incAction);
|
||||
}
|
||||
|
||||
template <typename TValue>
|
||||
void adjust_u16(TValue& value, int delta, u16 minValue, u16 maxValue) {
|
||||
const int nextValue = std::clamp(static_cast<int>(value) + delta, static_cast<int>(minValue), static_cast<int>(maxValue));
|
||||
const int nextValue = std::clamp(
|
||||
static_cast<int>(value) + delta, static_cast<int>(minValue), static_cast<int>(maxValue));
|
||||
value = static_cast<u16>(nextValue);
|
||||
}
|
||||
|
||||
@@ -116,9 +94,12 @@ void render_player_status_tab(Rml::Element* content, const Rml::String& activeSe
|
||||
Rml::String leftPane = R"RML(<div class="pane"><div class="section-heading">Player</div>)RML";
|
||||
leftPane += make_select_row("player_name", "Player Name", get_player_name(), activeSelection);
|
||||
leftPane += make_select_row("horse_name", "Horse Name", get_horse_name(), activeSelection);
|
||||
leftPane += make_select_row("max_health", "Max Health", value_for_player_selection("max_health"), activeSelection);
|
||||
leftPane += make_select_row("health", "Health", value_for_player_selection("health"), activeSelection);
|
||||
leftPane += make_select_row("max_oil", "Max Oil", value_for_player_selection("max_oil"), activeSelection);
|
||||
leftPane += make_select_row(
|
||||
"max_health", "Max Health", value_for_player_selection("max_health"), activeSelection);
|
||||
leftPane +=
|
||||
make_select_row("health", "Health", value_for_player_selection("health"), activeSelection);
|
||||
leftPane += make_select_row(
|
||||
"max_oil", "Max Oil", value_for_player_selection("max_oil"), activeSelection);
|
||||
leftPane += make_select_row("oil", "Oil", value_for_player_selection("oil"), activeSelection);
|
||||
leftPane += "</div>";
|
||||
|
||||
@@ -190,19 +171,71 @@ bool handle_editor_action(const Rml::VariantList& arguments) {
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorWindow::EditorWindow()
|
||||
: Window({.tabs = {
|
||||
{"Player Status",
|
||||
"player_name",
|
||||
[](Rml::Element* content, const Rml::String& activeSelection) { render_player_status_tab(content, activeSelection);
|
||||
}},
|
||||
{"Location",
|
||||
"",
|
||||
[](Rml::Element* content, const Rml::String&) { Rml::Factory::InstanceElementText(content, kLocationContent);
|
||||
}},
|
||||
{"Inventory"},
|
||||
},
|
||||
.actionHandler = handle_editor_action
|
||||
}){}
|
||||
EditorWindow::EditorWindow() {
|
||||
add_tab("Player Status", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
leftPane.add_section("Player");
|
||||
leftPane.add_select_button({
|
||||
.key = "Player Name",
|
||||
.getValue = get_player_name,
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Horse Name",
|
||||
.getValue = get_horse_name,
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Max Health",
|
||||
.getValue = [] { return value_for_player_selection("max_health"); },
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Max Oil",
|
||||
.getValue = [] { return value_for_player_selection("max_oil"); },
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Oil",
|
||||
.getValue = [] { return value_for_player_selection("oil"); },
|
||||
});
|
||||
leftPane.add_section("Equipment");
|
||||
leftPane.add_select_button({
|
||||
.key = "Equip X",
|
||||
.value = "TODO",
|
||||
.selected = true,
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Equip Y",
|
||||
.value = "TODO",
|
||||
.selected = false,
|
||||
});
|
||||
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
rightPane.add_button({
|
||||
.text = "Hello, world!",
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Location", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
|
||||
add_tab("Inventory", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
|
||||
add_tab("Collection", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
|
||||
add_tab("Flags", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
|
||||
add_tab("Minigame", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
|
||||
add_tab("Config", [this](Rml::Element* content) {
|
||||
// TODO
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "event.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
ScopedEventListener::ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture)
|
||||
: mElement(element), mEvent(event), mCapture(capture), mCallback(std::move(callback)) {
|
||||
mElement->AddEventListener(mEvent, this, mCapture);
|
||||
}
|
||||
|
||||
ScopedEventListener::~ScopedEventListener() {
|
||||
if (mElement != nullptr) {
|
||||
mElement->RemoveEventListener(mEvent, this, mCapture);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::ProcessEvent(Rml::Event& event) {
|
||||
if (mCallback) {
|
||||
mCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::OnDetach(Rml::Element* element) {
|
||||
if (element == mElement) {
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class ScopedEventListener final : public Rml::EventListener {
|
||||
public:
|
||||
using Callback = std::function<void(Rml::Event&)>;
|
||||
|
||||
ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
|
||||
~ScopedEventListener() override;
|
||||
|
||||
void ProcessEvent(Rml::Event& event) override;
|
||||
void OnDetach(Rml::Element* element) override;
|
||||
|
||||
private:
|
||||
Rml::Element* mElement = nullptr;
|
||||
Rml::EventId mEvent = Rml::EventId::Invalid;
|
||||
bool mCapture = false;
|
||||
Callback mCallback;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("div");
|
||||
elem->SetClass("pane", true);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Pane::Pane(Rml::Element* parent) : Component(createRoot(parent)) {}
|
||||
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("section-heading", true);
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Pane : public Component {
|
||||
public:
|
||||
explicit Pane(Rml::Element* parent);
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
Button& add_button(Button::Props props) { return add_child<Button>(mRoot, std::move(props)); }
|
||||
SelectButton& add_select_button(SelectButton::Props props) {
|
||||
return add_child<SelectButton>(mRoot, std::move(props));
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
void clear();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("button");
|
||||
elem->SetClass("select-button", true);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SelectButton::SelectButton(Rml::Element* parent, Props props) : Component(createRoot(parent)) {
|
||||
mKeyElem = append(mRoot, "div");
|
||||
mKeyElem->SetClass("key", true);
|
||||
mValueElem = append(mRoot, "div");
|
||||
mValueElem->SetClass("value", true);
|
||||
update_props(std::move(props));
|
||||
listen(mRoot, Rml::EventId::Click, [this](Rml::Event& event) {
|
||||
if (mProps.onPressed) {
|
||||
mProps.onPressed(*this, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SelectButton::update() {
|
||||
if (mProps.getValue) {
|
||||
set_value(mProps.getValue());
|
||||
}
|
||||
Component::update();
|
||||
}
|
||||
|
||||
void SelectButton::set_selected(bool selected) {
|
||||
if (mProps.selected != selected) {
|
||||
mRoot->SetClass("selected", selected);
|
||||
mProps.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::set_value(const Rml::String& value) {
|
||||
if (mProps.value != value) {
|
||||
mValueElem->SetInnerRML(escape(value));
|
||||
mProps.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::update_props(Props props) {
|
||||
if (mProps.key != props.key) {
|
||||
mKeyElem->SetInnerRML(escape(props.key));
|
||||
}
|
||||
set_value(props.value);
|
||||
set_selected(props.selected);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SelectButton;
|
||||
|
||||
struct SelectButtonProps {
|
||||
Rml::String key;
|
||||
Rml::String value;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(SelectButton& self, Rml::Event&)> onPressed;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
class SelectButton : public Component {
|
||||
public:
|
||||
using Props = SelectButtonProps;
|
||||
|
||||
SelectButton(Rml::Element* parent, SelectButtonProps props);
|
||||
|
||||
void update() override;
|
||||
|
||||
void set_selected(bool selected);
|
||||
bool get_selected() const { return mProps.selected; }
|
||||
|
||||
void set_value(const Rml::String& value);
|
||||
|
||||
private:
|
||||
void update_props(Props props);
|
||||
|
||||
SelectButtonProps mProps;
|
||||
Rml::Element* mKeyElem = nullptr;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "aurora/gfx.h"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "pane.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
SettingsWindow::SettingsWindow() {
|
||||
add_tab("Audio", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
|
||||
leftPane.add_section("Volume");
|
||||
{
|
||||
auto& btn = leftPane.add_select_button({
|
||||
.key = "Master Volume",
|
||||
.getValue =
|
||||
[] { return fmt::format("{}%", getSettings().audio.masterVolume.getValue()); },
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Focus, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Adjusts the volume of all sounds in the game.");
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Mouseover, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Adjusts the volume of all sounds in the game.");
|
||||
});
|
||||
}
|
||||
|
||||
leftPane.add_section("Effects");
|
||||
{
|
||||
auto& btn = leftPane.add_select_button({
|
||||
.key = "Enable Reverb",
|
||||
.getValue = [] { return getSettings().audio.enableReverb ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton& self, Rml::Event& event) {
|
||||
getSettings().audio.enableReverb.setValue(
|
||||
!getSettings().audio.enableReverb);
|
||||
dusk::audio::SetEnableReverb(getSettings().audio.enableReverb);
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Focus, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Enables the reverb effect in game audio.");
|
||||
});
|
||||
btn.listen(nullptr, Rml::EventId::Mouseover, [&](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Enables the reverb effect in game audio.");
|
||||
});
|
||||
}
|
||||
|
||||
leftPane.add_section("Tweaks");
|
||||
leftPane.add_select_button({
|
||||
.key = "No Low HP Sound",
|
||||
.value = "Off",
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Non-Stop Midna's Lament",
|
||||
.value = "On",
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Cheats", [this](Rml::Element* content) {
|
||||
|
||||
});
|
||||
|
||||
add_tab("Gameplay", [this](Rml::Element* content) {
|
||||
|
||||
});
|
||||
|
||||
add_tab("Graphics", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content);
|
||||
auto& rightPane = add_child<Pane>(content);
|
||||
|
||||
leftPane.add_section("Display");
|
||||
leftPane.add_button({
|
||||
.text = "Toggle Fullscreen",
|
||||
.onPressed =
|
||||
[](Rml::Event&) {
|
||||
getSettings().video.enableFullscreen.setValue(
|
||||
!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
leftPane.add_button({
|
||||
.text = "Restore Default Window Size",
|
||||
.onPressed =
|
||||
[](Rml::Event&) {
|
||||
getSettings().video.enableFullscreen.setValue(false);
|
||||
VISetWindowFullscreen(false);
|
||||
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
|
||||
VICenterWindow();
|
||||
},
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Enable VSync",
|
||||
.getValue = [] { return getSettings().video.enableVsync ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton&, Rml::Event&) {
|
||||
getSettings().video.enableVsync.setValue(!getSettings().video.enableVsync);
|
||||
aurora_enable_vsync(getSettings().video.enableVsync);
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
leftPane.add_select_button({
|
||||
.key = "Force 4:3 Aspect Ratio",
|
||||
.getValue = [] { return getSettings().video.lockAspectRatio ? "On" : "Off"; },
|
||||
.onPressed =
|
||||
[](SelectButton&, Rml::Event&) {
|
||||
getSettings().video.lockAspectRatio.setValue(
|
||||
!getSettings().video.lockAspectRatio);
|
||||
if (getSettings().video.lockAspectRatio) {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
|
||||
} else {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH);
|
||||
}
|
||||
config::Save();
|
||||
},
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SettingsWindow : public Window {
|
||||
public:
|
||||
SettingsWindow();
|
||||
};
|
||||
|
||||
}
|
||||
+50
-6
@@ -6,6 +6,8 @@
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -13,12 +15,13 @@ void load_font(const char* filename, bool fallback = false) {
|
||||
Rml::LoadFontFace(resource_path(filename).string(), fallback);
|
||||
}
|
||||
|
||||
bool sInitialized = false;
|
||||
std::vector<std::unique_ptr<Window> > sWindows;
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool s_initialized = false;
|
||||
|
||||
bool initialize() noexcept {
|
||||
if (s_initialized) {
|
||||
if (sInitialized) {
|
||||
return true;
|
||||
}
|
||||
if (!aurora::rmlui::is_initialized()) {
|
||||
@@ -29,22 +32,35 @@ bool initialize() noexcept {
|
||||
load_font("FiraSansCondensed-Regular.ttf");
|
||||
load_font("FiraSansCondensed-Bold.ttf");
|
||||
|
||||
s_initialized = true;
|
||||
sInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() noexcept {
|
||||
s_initialized = false;
|
||||
sWindows.clear();
|
||||
sInitialized = false;
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void update() noexcept {
|
||||
Window& add_window(std::unique_ptr<Window> window) noexcept {
|
||||
Window& ret = *window;
|
||||
sWindows.push_back(std::move(window));
|
||||
return ret;
|
||||
}
|
||||
|
||||
void remove_window(Window& window) noexcept {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void update() noexcept {
|
||||
for (const auto& window : sWindows) {
|
||||
window->update();
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (basePath == nullptr) {
|
||||
@@ -53,4 +69,32 @@ std::filesystem::path resource_path(const std::filesystem::path& filename) noexc
|
||||
return std::filesystem::path(basePath) / "res" / filename;
|
||||
}
|
||||
|
||||
std::string escape(std::string_view str) noexcept {
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
for (const char c : str) {
|
||||
switch (c) {
|
||||
case '&':
|
||||
result += "&";
|
||||
break;
|
||||
case '<':
|
||||
result += "<";
|
||||
break;
|
||||
case '>':
|
||||
result += ">";
|
||||
break;
|
||||
case '"':
|
||||
result += """;
|
||||
break;
|
||||
case '\'':
|
||||
result += "'";
|
||||
break;
|
||||
default:
|
||||
result += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <filesystem>
|
||||
|
||||
namespace dusk::ui {
|
||||
class Window;
|
||||
|
||||
bool initialize() noexcept;
|
||||
void shutdown() noexcept;
|
||||
@@ -12,6 +13,10 @@ void shutdown() noexcept;
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void update() noexcept;
|
||||
|
||||
Window& add_window(std::unique_ptr<Window> window) noexcept;
|
||||
void remove_window(Window& window) noexcept;
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||
std::string escape(std::string_view str) noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+50
-152
@@ -3,170 +3,20 @@
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
bool setup_window_model(Rml::Context* context, WindowModel& model, Rml::DataModelHandle& handle) {
|
||||
Rml::DataModelConstructor constructor = context->CreateDataModel("window");
|
||||
if (!constructor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto tab_handle = constructor.RegisterStruct<WindowTab>()) {
|
||||
tab_handle.RegisterMember("label", &WindowTab::label);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!constructor.RegisterArray<std::vector<WindowTab> >()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor.Bind("active_tab", &model.activeTab);
|
||||
constructor.Bind("tabs", &model.tabs);
|
||||
constructor.Bind("active_selection", &model.activeSelection);
|
||||
constructor.BindEventCallback("set_active_tab", &WindowModel::set_active_tab, &model);
|
||||
constructor.BindEventCallback("set_active_selection", &WindowModel::set_active_selection, &model);
|
||||
constructor.BindEventCallback("window_action", &WindowModel::handle_action, &model);
|
||||
|
||||
handle = constructor.GetModelHandle();
|
||||
return true;
|
||||
}
|
||||
|
||||
Rml::ElementDocument* get_document_from_event(Rml::Event& event) {
|
||||
auto* currentElem = event.GetCurrentElement();
|
||||
if (currentElem == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return currentElem->GetOwnerDocument();
|
||||
}
|
||||
|
||||
Rml::Element* get_content_element(Rml::ElementDocument* document) {
|
||||
if (document == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return document->GetElementById("content");
|
||||
}
|
||||
|
||||
void clear_children(Rml::Element* element) {
|
||||
if (element == nullptr) {
|
||||
return;
|
||||
}
|
||||
while (element->GetNumChildren() > 0) {
|
||||
element->RemoveChild(element->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
void ensure_tab_selection_state(WindowModel& model) {
|
||||
if (model.tabSelections.size() < model.tabs.size()) {
|
||||
model.tabSelections.resize(model.tabs.size());
|
||||
}
|
||||
if (model.activeTab < 0 || model.activeTab >= static_cast<int>(model.tabs.size())) {
|
||||
model.activeTab = 0;
|
||||
}
|
||||
if (model.tabs.empty()) {
|
||||
model.activeSelection.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::String& tabSelection = model.tabSelections[model.activeTab];
|
||||
if (tabSelection.empty()) {
|
||||
tabSelection = model.tabs[model.activeTab].defaultSelection;
|
||||
}
|
||||
model.activeSelection = tabSelection;
|
||||
}
|
||||
|
||||
void render_active_tab_content(WindowModel& model, Rml::ElementDocument* document) {
|
||||
auto* content = get_content_element(document);
|
||||
if (content == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_children(content);
|
||||
if (model.tabs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_tab_selection_state(model);
|
||||
const WindowTab& tab = model.tabs[model.activeTab];
|
||||
if (tab.setContent) {
|
||||
tab.setContent(content, model.activeSelection);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WindowModel::set_active_tab(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
|
||||
if (arguments.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int tabIndex = arguments[0].Get<int>();
|
||||
if (tabIndex < 0 || tabIndex >= static_cast<int>(tabs.size()) || tabIndex == activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeTab = tabIndex;
|
||||
ensure_tab_selection_state(*this);
|
||||
model.DirtyVariable("active_tab");
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
void WindowModel::set_active_selection(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
|
||||
if (arguments.empty() || tabs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Rml::String selection = arguments[0].Get<Rml::String>();
|
||||
ensure_tab_selection_state(*this);
|
||||
if (activeSelection == selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeSelection = selection;
|
||||
tabSelections[activeTab] = selection;
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
void WindowModel::handle_action(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
|
||||
bool shouldRerender = true;
|
||||
if (actionHandler) {
|
||||
shouldRerender = actionHandler(arguments);
|
||||
}
|
||||
if (!shouldRerender) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.DirtyVariable("active_tab");
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
Window::Window(WindowModel model) : mModel(std::move(model)) {
|
||||
Window::Window() {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setup_window_model(context, mModel, mModelHandle);
|
||||
|
||||
mDocument = context->LoadDocument("res/rml/window.rml");
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_tab_selection_state(mModel);
|
||||
render_active_tab();
|
||||
}
|
||||
|
||||
void Window::render_active_tab() noexcept {
|
||||
render_active_tab_content(mModel, mDocument);
|
||||
}
|
||||
|
||||
Window::~Window() {
|
||||
@@ -189,4 +39,52 @@ void Window::hide() {
|
||||
}
|
||||
}
|
||||
|
||||
void Window::update() {
|
||||
for (const auto& component : mContentComponents) {
|
||||
component->update();
|
||||
}
|
||||
}
|
||||
|
||||
void Window::set_active_tab(int index) {
|
||||
if (index < 0 || index >= mTabs.size() || index == mSelectedTabIndex) {
|
||||
return;
|
||||
}
|
||||
clear_content();
|
||||
for (int i = 0; i < mTabs.size(); i++) {
|
||||
mTabs[i].button->set_selected(i == index);
|
||||
}
|
||||
mSelectedTabIndex = index;
|
||||
const auto& tab = mTabs[index];
|
||||
if (tab.builder) {
|
||||
tab.builder(mDocument->GetElementById("content"));
|
||||
}
|
||||
}
|
||||
|
||||
void Window::add_tab(const Rml::String& title, TabBuilder builder) {
|
||||
const int index = static_cast<int>(mTabs.size());
|
||||
auto* tabBar = mDocument->GetElementById("tab-bar");
|
||||
mTabs.emplace_back(Tab{
|
||||
.title = title,
|
||||
.button = std::make_unique<Button>(tabBar,
|
||||
Button::Props{
|
||||
.text = title,
|
||||
.onPressed = [this, index](Rml::Event&) { set_active_tab(index); },
|
||||
.selected = index == mSelectedTabIndex,
|
||||
},
|
||||
"tab"),
|
||||
.builder = std::move(builder),
|
||||
});
|
||||
if (index == mSelectedTabIndex && builder) {
|
||||
builder(mDocument->GetElementById("content"));
|
||||
}
|
||||
}
|
||||
|
||||
void Window::clear_content() noexcept {
|
||||
mContentComponents.clear();
|
||||
auto* content = mDocument->GetElementById("content");
|
||||
while (content->GetNumChildren() != 0) {
|
||||
content->RemoveChild(content->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+31
-26
@@ -3,43 +3,48 @@
|
||||
#include <RmlUi/Core/DataModelHandle.h>
|
||||
#include <RmlUi/Core/ElementDocument.h>
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
struct WindowTab {
|
||||
Rml::String label;
|
||||
Rml::String defaultSelection;
|
||||
std::function<void(Rml::Element*, const Rml::String&)> setContent;
|
||||
};
|
||||
|
||||
struct WindowModel {
|
||||
int activeTab = 0;
|
||||
Rml::String activeSelection;
|
||||
std::vector<WindowTab> tabs;
|
||||
std::vector<Rml::String> tabSelections;
|
||||
std::function<bool(const Rml::VariantList&)> actionHandler;
|
||||
|
||||
void set_active_tab(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
void set_active_selection(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
void handle_action(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
};
|
||||
|
||||
class Window {
|
||||
public:
|
||||
Window(WindowModel model);
|
||||
using TabBuilder = std::function<void(Rml::Element*)>;
|
||||
struct Tab {
|
||||
Rml::String title;
|
||||
std::unique_ptr<Button> button;
|
||||
TabBuilder builder;
|
||||
};
|
||||
|
||||
Window();
|
||||
~Window();
|
||||
|
||||
Window(const Window&) = delete;
|
||||
Window& operator=(const Window&) = delete;
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
|
||||
private:
|
||||
void render_active_tab() noexcept;
|
||||
void update();
|
||||
void set_active_tab(int index);
|
||||
|
||||
protected:
|
||||
void add_tab(const Rml::String& title, TabBuilder builder);
|
||||
void clear_content() noexcept;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mContentComponents.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
WindowModel mModel;
|
||||
Rml::DataModelHandle mModelHandle;
|
||||
Rml::ElementDocument* mDocument = nullptr;
|
||||
std::vector<Tab> mTabs;
|
||||
std::vector<std::unique_ptr<Component> > mContentComponents;
|
||||
int mSelectedTabIndex = 0;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "dusk/ui/settings.hpp"
|
||||
|
||||
// --- GLOBALS ---
|
||||
s8 mDoMain::developmentMode = -1;
|
||||
OSTime mDoMain::sPowerOnTime;
|
||||
@@ -587,8 +589,10 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::ui::initialize();
|
||||
|
||||
// TODO: just for testing
|
||||
dusk::ui::EditorWindow editorWindow;
|
||||
editorWindow.show();
|
||||
auto& editorWindow = dusk::ui::add_window(std::make_unique<dusk::ui::EditorWindow>());
|
||||
// editorWindow.show();
|
||||
auto& settingsWindow = dusk::ui::add_window(std::make_unique<dusk::ui::SettingsWindow>());
|
||||
settingsWindow.show();
|
||||
|
||||
std::string dvd_path;
|
||||
bool dvd_opened = false;
|
||||
|
||||
Reference in New Issue
Block a user