Create new component system & initial settings window

This commit is contained in:
Luke Street
2026-04-29 00:38:26 -06:00
parent 1e372a856d
commit 3cb7fbd030
21 changed files with 777 additions and 252 deletions
+12
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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>
+49
View File
@@ -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
+30
View File
@@ -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
+45
View File
@@ -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
+49
View File
@@ -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
View File
@@ -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
+31
View File
@@ -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
+27
View File
@@ -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
+36
View File
@@ -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
+22
View File
@@ -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
+62
View File
@@ -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
+38
View File
@@ -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
+131
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "window.hpp"
namespace dusk::ui {
class SettingsWindow : public Window {
public:
SettingsWindow();
};
}
+50 -6
View File
@@ -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 += "&amp;";
break;
case '<':
result += "&lt;";
break;
case '>':
result += "&gt;";
break;
case '"':
result += "&quot;";
break;
case '\'':
result += "&apos;";
break;
default:
result += c;
break;
}
}
return result;
}
} // namespace dusk::ui
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -2
View File
@@ -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;