diff --git a/files.cmake b/files.cmake index 13fd5c9629..8aa6097119 100644 --- a/files.cmake +++ b/files.cmake @@ -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 diff --git a/res/rcss/window.rcss b/res/rml/window.rcss similarity index 99% rename from res/rcss/window.rcss rename to res/rml/window.rcss index 3858ae5166..2b572772e5 100644 --- a/res/rcss/window.rcss +++ b/res/rml/window.rcss @@ -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); diff --git a/res/rml/window.rml b/res/rml/window.rml index 93a4397ae1..a77728d00d 100644 --- a/res/rml/window.rml +++ b/res/rml/window.rml @@ -3,16 +3,9 @@ Window - -
-
- -
+ +
+
diff --git a/src/dusk/ui/button.cpp b/src/dusk/ui/button.cpp new file mode 100644 index 0000000000..d5c731e0f8 --- /dev/null +++ b/src/dusk/ui/button.cpp @@ -0,0 +1,49 @@ +#include "button.hpp" + +#include "ui.hpp" + +#include + +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 \ No newline at end of file diff --git a/src/dusk/ui/button.hpp b/src/dusk/ui/button.hpp new file mode 100644 index 0000000000..deaadde12b --- /dev/null +++ b/src/dusk/ui/button.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "component.hpp" + +namespace dusk::ui { + +struct ButtonProps { + Rml::String text; + std::function 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 \ No newline at end of file diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp new file mode 100644 index 0000000000..b4f6a287a9 --- /dev/null +++ b/src/dusk/ui/component.cpp @@ -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(element, event, std::move(callback), capture)); +} + +void Component::clear_children() { + mChildren.clear(); + while (mRoot->GetNumChildren() > 0) { + mRoot->RemoveChild(mRoot->GetFirstChild()); + } +} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp new file mode 100644 index 0000000000..9291a25da8 --- /dev/null +++ b/src/dusk/ui/component.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "event.hpp" + +#include + +#include +#include + +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 + requires std::is_base_of_v T& add_child(Args&&... args) { + auto child = std::make_unique(std::forward(args)...); + T& ref = *child; + mChildren.emplace_back(std::move(child)); + return ref; + } + + Rml::Element* mRoot = nullptr; + std::vector > mChildren; + std::vector > mListeners; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index bd818438ca..56ef554c3c 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -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( -
-
Save Location
- - - -
Horse Location
- -
-
-)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( "", - 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( "
" "
{0}
" @@ -100,15 +80,13 @@ Rml::String make_numeric_detail(std::string_view label, std::string_view decActi "" "
" "
", - label, - decAction, - incAction - ); + label, decAction, incAction); } template void adjust_u16(TValue& value, int delta, u16 minValue, u16 maxValue) { - const int nextValue = std::clamp(static_cast(value) + delta, static_cast(minValue), static_cast(maxValue)); + const int nextValue = std::clamp( + static_cast(value) + delta, static_cast(minValue), static_cast(maxValue)); value = static_cast(nextValue); } @@ -116,9 +94,12 @@ void render_player_status_tab(Rml::Element* content, const Rml::String& activeSe Rml::String leftPane = R"RML(
Player
)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 += "
"; @@ -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(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(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 diff --git a/src/dusk/ui/event.cpp b/src/dusk/ui/event.cpp new file mode 100644 index 0000000000..0135cd56b7 --- /dev/null +++ b/src/dusk/ui/event.cpp @@ -0,0 +1,31 @@ +#include "event.hpp" + +#include + +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 diff --git a/src/dusk/ui/event.hpp b/src/dusk/ui/event.hpp new file mode 100644 index 0000000000..27a318de3f --- /dev/null +++ b/src/dusk/ui/event.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace dusk::ui { + +class ScopedEventListener final : public Rml::EventListener { +public: + using Callback = std::function; + + 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 diff --git a/src/dusk/ui/pane.cpp b/src/dusk/ui/pane.cpp new file mode 100644 index 0000000000..d36d25ee30 --- /dev/null +++ b/src/dusk/ui/pane.cpp @@ -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 \ No newline at end of file diff --git a/src/dusk/ui/pane.hpp b/src/dusk/ui/pane.hpp new file mode 100644 index 0000000000..ac252bf54c --- /dev/null +++ b/src/dusk/ui/pane.hpp @@ -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