diff --git a/files.cmake b/files.cmake index 8aa6097119..eca3250d01 100644 --- a/files.cmake +++ b/files.cmake @@ -1470,12 +1470,16 @@ set(DUSK_FILES src/dusk/ui/editor.hpp src/dusk/ui/event.cpp src/dusk/ui/event.hpp + src/dusk/ui/number_button.cpp + src/dusk/ui/number_button.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/string_button.cpp + src/dusk/ui/string_button.hpp src/dusk/ui/ui.hpp src/dusk/ui/ui.cpp src/dusk/ui/window.hpp diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index fa1895b5ef..6f7342bc1b 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -1,9 +1,6 @@ #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) {} diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index d6818e3604..4c130620a0 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -1,19 +1,16 @@ #include "editor.hpp" #include +#include -#include "fmt/format.h" - -#include "aurora/lib/dolphin/gd/gd.hpp" #include "button.hpp" +#include "number_button.hpp" #include "pane.hpp" #include "select_button.hpp" - -#include +#include "string_button.hpp" namespace dusk::ui { namespace { -aurora::Module Log{"dusk::ui::editor"}; bool has_save_data() { return dComIfGs_getSaveData() != nullptr; @@ -181,218 +178,6 @@ bool handle_editor_action(const Rml::VariantList& arguments) { } // namespace -class ControlledSelectButton : public SelectButton { -public: - ControlledSelectButton(Rml::Element* parent, Props props) - : SelectButton(parent, std::move(props)) {} - - void update() override { - set_disabled(is_disabled()); - set_value_label(format_value()); - SelectButton::update(); - } - -protected: - virtual Rml::String format_value() = 0; - virtual bool is_disabled() { return false; } -}; - -class BaseStringButton : public ControlledSelectButton { -public: - struct Props { - Rml::String key; - Rml::String type = "text"; - int maxLength = -1; - }; - - BaseStringButton(Rml::Element* parent, Props props) - : ControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)), - mMaxLength(props.maxLength) { - mInputListeners.reserve(3); - } - - void update() override { - if (mPendingStopEditing) { - stop_editing(mPendingCommit, mPendingRefocusRoot); - } - ControlledSelectButton::update(); - } - - void start_editing() { - if (mInputElem != nullptr) { - return; - } - auto* doc = mRoot->GetOwnerDocument(); - auto elemPtr = doc->CreateElement("input"); - mInputElem = rmlui_dynamic_cast(elemPtr.get()); - if (mInputElem == nullptr) { - return; - } - mInputElem->SetAttribute("type", mType); - mInputElem->SetAttribute("value", format_value()); - if (mMaxLength > -1) { - mInputElem->SetAttribute("maxlength", mMaxLength); - } - mRoot->AppendChild(std::move(elemPtr)); - mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden); - mInputElem->Focus(true); - const int end = static_cast(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue())); - mInputElem->SetSelectionRange(0, end); - set_selected(true); - mInputListeners.emplace_back(std::make_unique( - mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) { - const auto cmd = map_nav_event(event); - if (cmd == NavCommand::Confirm) { - request_stop_editing(true, true); - event.StopImmediatePropagation(); - } else if (cmd == NavCommand::Cancel) { - request_stop_editing(false, true); - event.StopImmediatePropagation(); - } - })); - mInputListeners.emplace_back(std::make_unique( - mInputElem, Rml::EventId::Click, [](Rml::Event& event) { event.StopPropagation(); })); - mInputListeners.emplace_back(std::make_unique(mInputElem, - Rml::EventId::Blur, [this](Rml::Event&) { request_stop_editing(true, false); })); - } - - void request_stop_editing(bool commit, bool refocusRoot) { - mPendingStopEditing = true; - mPendingCommit = commit; - mPendingRefocusRoot = refocusRoot; - } - -protected: - bool handle_nav_command(NavCommand cmd) override { - if (cmd == NavCommand::Confirm) { - if (mInputElem == nullptr) { - start_editing(); - } else { - request_stop_editing(true, true); - } - return true; - } else if (cmd == NavCommand::Cancel) { - request_stop_editing(false, true); - return true; - } - return false; - } - - virtual void set_value(Rml::String value) = 0; - -private: - void stop_editing(bool commit = true, bool refocusRoot = false) { - if (mInputElem == nullptr) { - return; - } - mPendingStopEditing = false; - if (commit) { - set_value(mInputElem->GetValue()); - } - mInputListeners.clear(); - mRoot->RemoveChild(mInputElem); - mInputElem = nullptr; - mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible); - set_selected(false); - if (refocusRoot) { - mRoot->Focus(true); - } - } - - Rml::ElementFormControlInput* mInputElem = nullptr; - std::vector > mInputListeners; - Rml::String mType; - int mMaxLength; - bool mPendingStopEditing = false; - bool mPendingCommit = true; - bool mPendingRefocusRoot = false; -}; - -class StringButton : public BaseStringButton { -public: - struct Props { - Rml::String key; - std::function getValue; - std::function setValue; - int maxLength = -1; - }; - - StringButton(Rml::Element* parent, Props props) - : BaseStringButton(parent, - { - .key = std::move(props.key), - .maxLength = props.maxLength, - }), - mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {} - -protected: - Rml::String format_value() override { return mGetValue(); } - void set_value(Rml::String value) override { - if (mSetValue) { - mSetValue(std::move(value)); - } - } - -private: - std::function mGetValue; - std::function mSetValue; -}; - -struct IntSelectProps { - Rml::String key; - std::function getValue; - std::function setValue; - int min = 0; - int max = INT_MAX; - int step = 1; -}; - -class IntSelectButton : public BaseStringButton { -public: - using Props = IntSelectProps; - - IntSelectButton(Rml::Element* parent, Props props) - : BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}), - mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), - mMin(props.min), mMax(props.max), mStep(props.step) {} - -protected: - Rml::String format_value() override { return fmt::to_string(mGetValue()); } - void set_value(Rml::String value) override { - if (!mSetValue) { - return; - } - - int parsedValue = 0; - const char* begin = value.data(); - const char* end = begin + value.size(); - const auto result = std::from_chars(begin, end, parsedValue); - if (result.ec != std::errc() || result.ptr != end) { - return; - } - - mSetValue(std::clamp(parsedValue, mMin, mMax)); - } - - bool handle_nav_command(NavCommand cmd) override { - if (cmd == NavCommand::Left) { - mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax)); - return true; - } else if (cmd == NavCommand::Right) { - mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax)); - return true; - } - return BaseStringButton::handle_nav_command(cmd); - } - -private: - std::function mGetValue; - std::function mSetValue; - int mMin; - int mMax; - int mStep; -}; - EditorWindow::EditorWindow() { add_tab("Player Status", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Direction::Vertical); @@ -411,33 +196,33 @@ EditorWindow::EditorWindow() { .setValue = set_horse_name, .maxLength = 16, }); - leftPane.add_child(leftPane.root(), - IntSelectButton::Props{ + leftPane.add_child(leftPane.root(), + NumberButton::Props{ .key = "Max Health", .getValue = [] { return get_player_status()->getMaxLife(); }, .setValue = [](int value) { return get_player_status()->setMaxLife(value); }, - .max = UINT16_MAX, // TODO: actual max + .max = UINT16_MAX, // TODO: actual max }); - leftPane.add_child(leftPane.root(), - IntSelectButton::Props{ + leftPane.add_child(leftPane.root(), + NumberButton::Props{ .key = "Health", .getValue = [] { return get_player_status()->getLife(); }, .setValue = [](int value) { return get_player_status()->setLife(value); }, - .max = UINT16_MAX, // TODO: actual max + .max = UINT16_MAX, // TODO: actual max }); - leftPane.add_child(leftPane.root(), - IntSelectButton::Props{ + leftPane.add_child(leftPane.root(), + NumberButton::Props{ .key = "Max Oil", .getValue = [] { return get_player_status()->getMaxOil(); }, .setValue = [](int value) { return get_player_status()->setMaxOil(value); }, - .max = UINT16_MAX, // TODO: actual max + .max = UINT16_MAX, // TODO: actual max }); - leftPane.add_child(leftPane.root(), - IntSelectButton::Props{ + leftPane.add_child(leftPane.root(), + NumberButton::Props{ .key = "Oil", .getValue = [] { return get_player_status()->getOil(); }, .setValue = [](int value) { return get_player_status()->setOil(value); }, - .max = UINT16_MAX, // TODO: actual max + .max = UINT16_MAX, // TODO: actual max }); leftPane.add_section("Equipment"); diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp new file mode 100644 index 0000000000..c94afede1a --- /dev/null +++ b/src/dusk/ui/number_button.cpp @@ -0,0 +1,44 @@ +#include "number_button.hpp" + +#include +#include + +namespace dusk::ui { + +NumberButton::NumberButton(Rml::Element* parent, Props props) + : BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}), + mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), mMin(props.min), + mMax(props.max), mStep(props.step) {} + +Rml::String NumberButton::format_value() { + return fmt::to_string(mGetValue()); +} + +void NumberButton::set_value(Rml::String value) { + if (!mSetValue) { + return; + } + + int parsedValue = 0; + const char* begin = value.data(); + const char* end = begin + value.size(); + const auto result = std::from_chars(begin, end, parsedValue); + if (result.ec != std::errc() || result.ptr != end) { + return; + } + + mSetValue(std::clamp(parsedValue, mMin, mMax)); +} + +bool NumberButton::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Left) { + mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax)); + return true; + } else if (cmd == NavCommand::Right) { + mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax)); + return true; + } + return BaseStringButton::handle_nav_command(cmd); +} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/number_button.hpp b/src/dusk/ui/number_button.hpp new file mode 100644 index 0000000000..a2665e24ac --- /dev/null +++ b/src/dusk/ui/number_button.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "string_button.hpp" + +namespace dusk::ui { + +class NumberButton : public BaseStringButton { +public: + struct Props { + Rml::String key; + std::function getValue; + std::function setValue; + int min = 0; + int max = INT_MAX; + int step = 1; + }; + + NumberButton(Rml::Element* parent, Props props); + +protected: + Rml::String format_value() override; + void set_value(Rml::String value) override; + bool handle_nav_command(NavCommand cmd) override; + +private: + std::function mGetValue; + std::function mSetValue; + int mMin; + int mMax; + int mStep; +}; + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/select_button.cpp b/src/dusk/ui/select_button.cpp index 9413124926..707f605e82 100644 --- a/src/dusk/ui/select_button.cpp +++ b/src/dusk/ui/select_button.cpp @@ -94,4 +94,10 @@ bool SelectButton::handle_nav_command(NavCommand cmd) { return false; } +void ControlledSelectButton::update() { + set_disabled(is_disabled()); + set_value_label(format_value()); + SelectButton::update(); +} + } // namespace dusk::ui diff --git a/src/dusk/ui/select_button.hpp b/src/dusk/ui/select_button.hpp index 880f2fbb55..20613d5b63 100644 --- a/src/dusk/ui/select_button.hpp +++ b/src/dusk/ui/select_button.hpp @@ -5,20 +5,16 @@ namespace dusk::ui { -class SelectButton; - -struct SelectButtonProps { - Rml::String key; - Rml::String value; - bool selected = false; - bool disabled = false; -}; - class SelectButton : public Component { public: - using Props = SelectButtonProps; + struct Props { + Rml::String key; + Rml::String value; + bool selected = false; + bool disabled = false; + }; - SelectButton(Rml::Element* parent, SelectButtonProps props); + SelectButton(Rml::Element* parent, Props props); bool focus() override; @@ -26,16 +22,27 @@ public: bool get_selected() const { return mProps.selected; } void set_disabled(bool disabled); bool get_disabled() const { return mProps.disabled; } - void set_value_label(const Rml::String& value); protected: void update_props(Props props); virtual bool handle_nav_command(NavCommand cmd); - SelectButtonProps mProps; + Props mProps; Rml::Element* mKeyElem = nullptr; Rml::Element* mValueElem = nullptr; }; +class ControlledSelectButton : public SelectButton { +public: + ControlledSelectButton(Rml::Element* parent, Props props) + : SelectButton(parent, std::move(props)) {} + + void update() override; + +protected: + virtual Rml::String format_value() = 0; + virtual bool is_disabled() { return false; } +}; + } // namespace dusk::ui diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp new file mode 100644 index 0000000000..60b80b437e --- /dev/null +++ b/src/dusk/ui/string_button.cpp @@ -0,0 +1,112 @@ +#include "string_button.hpp" + +namespace dusk::ui { + +BaseStringButton::BaseStringButton(Rml::Element* parent, Props props) + : ControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)), + mMaxLength(props.maxLength) { + mInputListeners.reserve(3); +} + +void BaseStringButton::update() { + if (mPendingStopEditing) { + stop_editing(mPendingCommit, mPendingRefocusRoot); + } + ControlledSelectButton::update(); +} + +void BaseStringButton::start_editing() { + if (mInputElem != nullptr) { + return; + } + + // Create input element + auto* doc = mRoot->GetOwnerDocument(); + auto elemPtr = doc->CreateElement("input"); + mInputElem = rmlui_dynamic_cast(elemPtr.get()); + if (mInputElem == nullptr) { + return; + } + mInputElem->SetAttribute("type", mType); + mInputElem->SetAttribute("value", format_value()); + if (mMaxLength > -1) { + mInputElem->SetAttribute("maxlength", mMaxLength); + } + mRoot->AppendChild(std::move(elemPtr)); + + // Hide value element + mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden); + + // Focus and select text within input + mInputElem->Focus(true); + const int end = static_cast(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue())); + mInputElem->SetSelectionRange(0, end); + + // Mark button as selected to indicate "active" + set_selected(true); + + // Register input listeners + mInputListeners.emplace_back(std::make_unique( + mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto cmd = map_nav_event(event); + if (cmd == NavCommand::Confirm) { + request_stop_editing(true, true); + event.StopImmediatePropagation(); + } else if (cmd == NavCommand::Cancel) { + request_stop_editing(false, true); + event.StopImmediatePropagation(); + } + })); + mInputListeners.emplace_back(std::make_unique( + mInputElem, Rml::EventId::Click, [](Rml::Event& event) { event.StopPropagation(); })); + mInputListeners.emplace_back(std::make_unique(mInputElem, + Rml::EventId::Blur, [this](Rml::Event&) { request_stop_editing(true, false); })); +} + +void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) { + mPendingStopEditing = true; + mPendingCommit = commit; + mPendingRefocusRoot = refocusRoot; +} + +bool BaseStringButton::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Confirm) { + if (mInputElem == nullptr) { + start_editing(); + } else { + request_stop_editing(true, true); + } + return true; + } else if (cmd == NavCommand::Cancel) { + request_stop_editing(false, true); + return true; + } + return false; +} + +void BaseStringButton::stop_editing(bool commit, bool refocusRoot) { + if (mInputElem == nullptr) { + return; + } + mPendingStopEditing = false; + if (commit) { + set_value(mInputElem->GetValue()); + } + mInputListeners.clear(); + mRoot->RemoveChild(mInputElem); + mInputElem = nullptr; + + // Restore value element + mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible); + + set_selected(false); + if (refocusRoot) { + mRoot->Focus(true); + } +} + +StringButton::StringButton(Rml::Element* parent, Props props) + : BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}), + mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/string_button.hpp b/src/dusk/ui/string_button.hpp new file mode 100644 index 0000000000..7fbd7350dc --- /dev/null +++ b/src/dusk/ui/string_button.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "select_button.hpp" + +#include + +namespace dusk::ui { + +class BaseStringButton : public ControlledSelectButton { +public: + struct Props { + Rml::String key; + Rml::String type = "text"; + int maxLength = -1; + }; + + BaseStringButton(Rml::Element* parent, Props props); + void update() override; + void start_editing(); + void request_stop_editing(bool commit, bool refocusRoot); + +protected: + bool handle_nav_command(NavCommand cmd) override; + virtual void set_value(Rml::String value) = 0; + +private: + void stop_editing(bool commit = true, bool refocusRoot = false); + + Rml::ElementFormControlInput* mInputElem = nullptr; + std::vector > mInputListeners; + Rml::String mType; + int mMaxLength; + bool mPendingStopEditing = false; + bool mPendingCommit = true; + bool mPendingRefocusRoot = false; +}; + +class StringButton : public BaseStringButton { +public: + struct Props { + Rml::String key; + std::function getValue; + std::function setValue; + int maxLength = -1; + }; + + StringButton(Rml::Element* parent, Props props); + +protected: + Rml::String format_value() override { return mGetValue(); } + void set_value(Rml::String value) override { + if (mSetValue) { + mSetValue(std::move(value)); + } + } + +private: + std::function mGetValue; + std::function mSetValue; +}; + +} // namespace dusk::ui \ No newline at end of file