diff --git a/res/rcss/window.rcss b/res/rcss/window.rcss index 1596be869e..3858ae5166 100644 --- a/res/rcss/window.rcss +++ b/res/rcss/window.rcss @@ -77,6 +77,23 @@ body { border-right: 1dp #92875B; } +.window .content .pane.detail-pane { + gap: 16dp; +} + +.window .content .detail-value { + padding: 12dp 16dp; + border-radius: 12dp; + background-color: rgba(17, 16, 10, 20%); + box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + font-size: 20dp; +} + +.window .content .detail-controls { + display: flex; + gap: 12dp; +} + .section-heading { font-weight: bold; text-transform: uppercase; diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index 4b7a05dd2d..bd818438ca 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -2,72 +2,11 @@ #include +#include "fmt/format.h" + namespace dusk::ui { namespace { -const Rml::String kPlayerStatusContent = R"RML( -
-
Player
- - - - - - -
Equipment
- - - - - -
- -
- - - - - - - - - -
-)RML"; - const Rml::String kLocationContent = R"RML(
Save Location
@@ -92,21 +31,178 @@ const Rml::String kLocationContent = R"RML(
)RML"; +bool has_save_data() { + return dComIfGs_getSaveData() != nullptr; +} + +dSv_player_status_a_c* get_player_status() { + if (!has_save_data()) { + return nullptr; + } + return &dComIfGs_getSaveData()->getPlayer().getPlayerStatusA(); +} + +Rml::String get_player_name() { + if (!has_save_data()) { + return nullptr; + } + return dComIfGs_getPlayerName(); +} + +Rml::String get_horse_name() { + if (!has_save_data()) { + return nullptr; + } + return dComIfGs_getHorseName(); +} + +Rml::String value_for_player_selection(const Rml::String& selection) { + dSv_player_status_a_c* status = get_player_status(); + if (selection == "get_horse_name") { + return get_horse_name(); + } + if (status == nullptr) { + return "?"; + } + if (selection == "max_health") { + return fmt::format("{}", static_cast(status->mMaxLife)); + } + if (selection == "health") { + return fmt::format("{}", static_cast(status->mLife)); + } + if (selection == "max_oil") { + return fmt::format("{}", static_cast(status->mMaxOil)); + } + if (selection == "oil") { + return fmt::format("{}", static_cast(status->mOil)); + } + return "Unknown"; +} + +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 + ); +} + +Rml::String make_numeric_detail(std::string_view label, std::string_view decAction, std::string_view incAction) { + return fmt::format( + "
" + "
{0}
" + "
" + "" + "" + "
" + "
", + 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)); + value = static_cast(nextValue); +} + +void render_player_status_tab(Rml::Element* content, const Rml::String& activeSelection) { + 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("oil", "Oil", value_for_player_selection("oil"), activeSelection); + leftPane += "
"; + + Rml::String rightPane; + if (activeSelection == "max_health") { + rightPane = make_numeric_detail("Max Health", "max_health.dec", "max_health.inc"); + } else if (activeSelection == "health") { + rightPane = make_numeric_detail("Health", "health.dec", "health.inc"); + } else if (activeSelection == "max_oil") { + rightPane = make_numeric_detail("Max Oil", "max_oil.dec", "max_oil.inc"); + } else if (activeSelection == "oil") { + rightPane = make_numeric_detail("Oil", "oil.dec", "oil.inc"); + } + + Rml::Factory::InstanceElementText(content, leftPane + rightPane); +} + +bool handle_editor_action(const Rml::VariantList& arguments) { + if (arguments.empty() || !has_save_data()) { + return true; + } + + const Rml::String action = arguments[0].Get(); + dSv_player_status_a_c* status = get_player_status(); + if (status == nullptr) { + return true; + } + + if (action == "max_health.inc") { + adjust_u16(status->mMaxLife, 1, 0, 0xFFFF); + return true; + } else if (action == "max_health.dec") { + adjust_u16(status->mMaxLife, -1, 0, 0xFFFF); + if (status->mLife > status->mMaxLife) { + status->mLife = status->mMaxLife; + } + return true; + } + + if (action == "health.inc") { + adjust_u16(status->mLife, 1, 0, status->mMaxLife); + return true; + } else if (action == "health.dec") { + adjust_u16(status->mLife, -1, 0, status->mMaxLife); + return true; + } + + if (action == "max_oil.inc") { + adjust_u16(status->mMaxOil, 1, 0, 0xFFFF); + return true; + } else if (action == "max_oil.dec") { + adjust_u16(status->mMaxOil, -1, 0, 0xFFFF); + if (status->mOil > status->mMaxOil) { + status->mOil = status->mMaxOil; + } + return true; + } + + if (action == "oil.inc") { + adjust_u16(status->mOil, 1, 0, status->mMaxOil); + return true; + } else if (action == "oil.dec") { + adjust_u16(status->mOil, -1, 0, status->mMaxOil); + return true; + } + + return false; +} + } // 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"}, - }}) {} + {"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 +}){} -} // namespace dusk::ui \ No newline at end of file +} // namespace dusk::ui diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp index e7ac3eb3c6..82d207f369 100644 --- a/src/dusk/ui/window.cpp +++ b/src/dusk/ui/window.cpp @@ -25,12 +25,76 @@ bool setup_window_model(Rml::Context* context, WindowModel& model, Rml::DataMode 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(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( @@ -45,27 +109,43 @@ void WindowModel::set_active_tab( } 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)); +} - // Replace window content with new tab content - auto* currentElem = event.GetCurrentElement(); - if (currentElem == nullptr) { +void WindowModel::set_active_selection( + Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) { + if (arguments.empty() || tabs.empty()) { return; } - auto* doc = currentElem->GetOwnerDocument(); - if (doc == nullptr) { + + const Rml::String selection = arguments[0].Get(); + ensure_tab_selection_state(*this); + if (activeSelection == selection) { return; } - auto* content = doc->GetElementById("content"); - if (content == nullptr) { + + 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; } - while (content->GetNumChildren() > 0) { - content->RemoveChild(content->GetFirstChild()); - } - if (tabs[tabIndex].setContent) { - tabs[tabIndex].setContent(content); - } + + 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)) { @@ -73,12 +153,20 @@ Window::Window(WindowModel model) : mModel(std::move(model)) { if (context == nullptr) { return; } + setup_window_model(context, mModel, mModelHandle); + mDocument = context->LoadDocument("res/rml/window.rml"); if (mDocument == nullptr) { return; } - mModel.tabs[0].setContent(mDocument->GetElementById("content")); + + ensure_tab_selection_state(mModel); + render_active_tab(); +} + +void Window::render_active_tab() noexcept { + render_active_tab_content(mModel, mDocument); } Window::~Window() { diff --git a/src/dusk/ui/window.hpp b/src/dusk/ui/window.hpp index 2faa4fb140..e4eb2c5ef6 100644 --- a/src/dusk/ui/window.hpp +++ b/src/dusk/ui/window.hpp @@ -7,15 +7,23 @@ namespace dusk::ui { struct WindowTab { Rml::String label; - std::function setContent; + Rml::String defaultSelection; + std::function setContent; }; struct WindowModel { int activeTab = 0; + Rml::String activeSelection; std::vector tabs; + std::vector tabSelections; + std::function 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 { @@ -27,9 +35,11 @@ public: void hide(); private: + void render_active_tab() noexcept; + WindowModel mModel; Rml::DataModelHandle mModelHandle; - Rml::ElementDocument* mDocument; + Rml::ElementDocument* mDocument = nullptr; }; -} // namespace dusk::ui \ No newline at end of file +} // namespace dusk::ui