diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 60025d82..a965e1e8 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -108,7 +108,7 @@ void GUI::actMouse(float delta) { doubleClicked = false; doubleClickTimer += delta + mouseDelta * 0.1f; - auto hover = container->getAt(Events::cursor, nullptr); + auto hover = container->getAt(Events::cursor); if (this->hover && this->hover != hover) { this->hover->setHover(false); } diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index ddf12de7..3fc25f66 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -17,9 +17,7 @@ Container::~Container() { Container::clear(); } -std::shared_ptr Container::getAt( - const glm::vec2& pos, const std::shared_ptr& self -) { +std::shared_ptr Container::getAt(const glm::vec2& pos) { if (!isInteractive() || !isEnabled()) { return nullptr; } @@ -28,19 +26,19 @@ std::shared_ptr Container::getAt( } int diff = (actualLength-size.y); if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) { - return UINode::getAt(pos, self); + return UINode::getAt(pos); } for (int i = nodes.size()-1; i >= 0; i--) { auto& node = nodes[i]; if (!node->isVisible()) continue; - auto hover = node->getAt(pos, node); + auto hover = node->getAt(pos); if (hover != nullptr) { return hover; } } - return UINode::getAt(pos, self); + return UINode::getAt(pos); } void Container::mouseMove(GUI* gui, int x, int y) { diff --git a/src/graphics/ui/elements/Container.hpp b/src/graphics/ui/elements/Container.hpp index 79d3fe62..f91fa915 100644 --- a/src/graphics/ui/elements/Container.hpp +++ b/src/graphics/ui/elements/Container.hpp @@ -28,7 +28,7 @@ namespace gui { virtual void act(float delta) override; virtual void drawBackground(const DrawContext& pctx, const Assets& assets); virtual void draw(const DrawContext& pctx, const Assets& assets) override; - virtual std::shared_ptr getAt(const glm::vec2& pos, const std::shared_ptr& self) override; + virtual std::shared_ptr getAt(const glm::vec2& pos) override; virtual void add(const std::shared_ptr& node); virtual void add(const std::shared_ptr& node, glm::vec2 pos); virtual void clear(); diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index 609231c0..88b0c86f 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -13,18 +13,177 @@ #include "util/stringutil.hpp" #include "window/Events.hpp" #include "window/Window.hpp" +#include "devtools/actions.hpp" #include "../markdown.hpp" using namespace gui; inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40; -TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) - : Container(glm::vec2(200,32)), - padding(padding), - input(L""), - placeholder(std::move(placeholder)) -{ +class InputAction : public Action { + std::weak_ptr textbox; + size_t position; + std::wstring string; +public: + InputAction( + std::weak_ptr textbox, size_t position, std::wstring string + ) + : textbox(std::move(textbox)), + position(position), + string(std::move(string)) { + } + void apply() override { + if (auto box = textbox.lock()) { + box->select(position, position); + box->paste(string); + } + } + + void revert() override { + if (auto box = textbox.lock()) { + box->select(position, position); + box->erase(position, string.length()); + } + } +}; + +class SelectionAction : public Action { + std::weak_ptr textbox; + size_t start; + size_t end; +public: + SelectionAction(std::weak_ptr textbox, size_t start, size_t end) + : textbox(std::move(textbox)), start(start), end(end) {} + + void apply() override { + if (auto box = textbox.lock()) { + box->select(start, end); + } + } + + void revert() override { + if (auto box = textbox.lock()) { + box->select(0, 0); + } + } +}; + +namespace gui { + /// @brief Accumulates small changes into words for InputAction creation + class TextBoxHistorian { + public: + TextBoxHistorian(TextBox& textBox, ActionsHistory& history) + : textBox(textBox), history(history) { + } + + void onPaste(size_t pos, std::wstring_view text) { + if (locked) { + return; + } + if (erasing) { + sync(); + } + if (this->pos == static_cast(-1)) { + this->pos = pos; + } + if (this->pos + length != pos || text == L" " || text == L"\n") { + sync(); + this->pos = pos; + } + ss << text; + length += text.length(); + } + + void onErase(size_t pos, std::wstring_view text, bool selection=false) { + if (locked) { + return; + } + if (!erasing) { + sync(); + erasing = true; + } + if (selection) { + history.store( + std::make_unique( + getTextBoxWeakptr(), + textBox.getSelectionStart(), + textBox.getSelectionEnd() + ), + true + ); + } + if (this->pos == static_cast(-1)) { + this->pos = pos; + } else if (this->pos - text.length() != pos) { + sync(); + erasing = true; + this->pos = pos; + } + if (text == L" " || text == L"\n") { + sync(); + erasing = true; + this->pos = pos; + } + auto str = ss.str(); + ss.seekp(0); + ss << text << str; + + this->pos = pos; + length += text.length(); + } + + /// @brief Flush buffer and push all changes to the ActionsHistory + void sync() { + auto string = ss.str(); + if (string.empty()) { + return; + } + auto action = + std::make_unique(getTextBoxWeakptr(), pos, string); + history.store(std::move(action), erasing); + pos = -1; + length = 0; + ss = {}; + erasing = false; + } + + void undo() { + sync(); + locked = true; + history.undo(); + locked = false; + } + + void redo() { + sync(); + locked = true; + history.redo(); + locked = false; + } + private: + TextBox& textBox; + ActionsHistory& history; + std::wstringstream ss; + size_t pos = -1; + size_t length = 0; + bool erasing = false; + bool locked = false; + + std::weak_ptr getTextBoxWeakptr() { + return std::weak_ptr(std::dynamic_pointer_cast( + textBox.shared_from_this() + )); + } + }; +} + +TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) + : Container(glm::vec2(200, 32)), + history(std::make_shared()), + historian(std::make_unique(*this, *history)), + padding(padding), + input(L""), + placeholder(std::move(placeholder)) { setCursor(CursorShape::TEXT); setOnUpPressed(nullptr); setOnDownPressed(nullptr); @@ -49,6 +208,8 @@ TextBox::TextBox(std::wstring placeholder, glm::vec4 padding) scrollStep = 0; } +TextBox::~TextBox() = default; + void TextBox::draw(const DrawContext& pctx, const Assets& assets) { Container::draw(pctx, assets); @@ -71,6 +232,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); batch->texture(nullptr); batch->setColor(glm::vec4(1.0f)); + if (editable && int((Window::time() - caretLastMove) * 2) % 2 == 0) { uint line = rawTextCache.getLineByTextIndex(caret); uint lcaret = caret - rawTextCache.getTextLineOffset(line); @@ -260,18 +422,22 @@ void TextBox::refreshLabel() { /// @brief Insert text at the caret. Also selected text will be erased /// @param text Inserting text -void TextBox::paste(const std::wstring& text) { +void TextBox::paste(const std::wstring& text, bool history) { eraseSelected(); + auto inputText = text; + inputText.erase( + std::remove(inputText.begin(), inputText.end(), '\r'), inputText.end() + ); + historian->onPaste(caret, inputText); if (caret >= input.length()) { - input += text; + input += inputText; } else { auto left = input.substr(0, caret); auto right = input.substr(caret); - input = left + text + right; + input = left + inputText + right; } - input.erase(std::remove(input.begin(), input.end(), '\r'), input.end()); refreshLabel(); - setCaret(caret + text.length()); + setCaret(caret + inputText.length()); if (validate()) { onInput(); } @@ -296,6 +462,11 @@ bool TextBox::eraseSelected() { if (selectionStart == selectionEnd) { return false; } + historian->onErase( + selectionStart, + input.substr(selectionStart, selectionEnd - selectionStart), + true + ); erase(selectionStart, selectionEnd-selectionStart); resetSelection(); onInput(); @@ -336,7 +507,9 @@ void TextBox::setTextOffset(uint x) { void TextBox::typed(unsigned int codepoint) { if (editable) { - paste(std::wstring({(wchar_t)codepoint})); + // Combine deleting selected text and inserting a symbol + auto combination = history->beginCombination(); + paste(std::wstring({static_cast(codepoint)})); } } @@ -383,6 +556,15 @@ bool TextBox::isEditable() const { return editable; } +size_t TextBox::getSelectionStart() const { + return selectionStart; +} + +size_t TextBox::getSelectionEnd() const { + return selectionEnd; +} + + void TextBox::setOnEditStart(runnable oneditstart) { onEditStart = oneditstart; } @@ -615,6 +797,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) { if (caret > input.length()) { caret = input.length(); } + historian->onErase(caret - 1, input.substr(caret - 1, caret)); input = input.substr(0, caret-1) + input.substr(caret); setCaret(caret-1); if (validate()) { @@ -623,6 +806,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) { } } else if (key == keycode::DELETE) { if (!eraseSelected() && caret < input.length()) { + historian->onErase(caret, input.substr(caret, caret + 1)); input = input.substr(0, caret) + input.substr(caret + 1); if (validate()) { onInput(); @@ -669,7 +853,11 @@ void TextBox::keyPressed(keycode key) { if (key == keycode::V && editable) { const char* text = Window::getClipboardText(); if (text) { + historian->sync(); // flush buffer before combination + // Combine deleting selected text and pasing a clipboard content + auto combination = history->beginCombination(); paste(util::str2wstr_utf8(text)); + historian->sync(); } } // Select/deselect all @@ -680,6 +868,12 @@ void TextBox::keyPressed(keycode key) { resetSelection(); } } + if (key == keycode::Z) { + historian->undo(); + } + if (key == keycode::Y) { + historian->redo(); + } } } @@ -704,10 +898,8 @@ size_t TextBox::getLinePos(uint line) const { return label->getTextLineOffset(line); } -std::shared_ptr TextBox::getAt( - const glm::vec2& pos, const std::shared_ptr& self -) { - return UINode::getAt(pos, self); +std::shared_ptr TextBox::getAt(const glm::vec2& pos) { + return UINode::getAt(pos); } void TextBox::setOnUpPressed(const runnable &callback) { diff --git a/src/graphics/ui/elements/TextBox.hpp b/src/graphics/ui/elements/TextBox.hpp index 7dd77525..907c040b 100644 --- a/src/graphics/ui/elements/TextBox.hpp +++ b/src/graphics/ui/elements/TextBox.hpp @@ -4,10 +4,14 @@ #include "Label.hpp" class Font; +class ActionsHistory; namespace gui { + class TextBoxHistorian; class TextBox : public Container { LabelCache rawTextCache; + std::shared_ptr history; + std::unique_ptr historian; protected: glm::vec4 focusedColor {0.0f, 0.0f, 0.0f, 1.0f}; glm::vec4 invalidColor {0.1f, 0.05f, 0.03f, 1.0f}; @@ -68,7 +72,6 @@ namespace gui { int calcIndexAt(int x, int y) const; void setTextOffset(uint x); - void erase(size_t start, size_t length); bool eraseSelected(); void resetSelection(); void extendSelection(int index); @@ -93,8 +96,11 @@ namespace gui { std::wstring placeholder, glm::vec4 padding=glm::vec4(4.0f) ); + + virtual ~TextBox(); - void paste(const std::wstring& text); + void paste(const std::wstring& text, bool history=true); + void erase(size_t start, size_t length); virtual void setTextSupplier(wstringsupplier supplier); @@ -201,6 +207,9 @@ namespace gui { virtual void setPadding(glm::vec4 padding); glm::vec4 getPadding() const; + size_t getSelectionStart() const; + size_t getSelectionEnd() const; + /// @brief Set runnable called on textbox focus virtual void setOnEditStart(runnable oneditstart); @@ -221,9 +230,7 @@ namespace gui { virtual void drawBackground(const DrawContext& pctx, const Assets& assets) override; virtual void typed(unsigned int codepoint) override; virtual void keyPressed(keycode key) override; - virtual std::shared_ptr getAt( - const glm::vec2& pos, const std::shared_ptr& self - ) override; + virtual std::shared_ptr getAt(const glm::vec2& pos) override; virtual void setOnUpPressed(const runnable &callback); virtual void setOnDownPressed(const runnable &callback); diff --git a/src/graphics/ui/elements/UINode.cpp b/src/graphics/ui/elements/UINode.cpp index 8dc04f68..520a3256 100644 --- a/src/graphics/ui/elements/UINode.cpp +++ b/src/graphics/ui/elements/UINode.cpp @@ -111,11 +111,11 @@ bool UINode::isInside(glm::vec2 point) { point.x < pos.x + size.x && point.y < pos.y + size.y); } -std::shared_ptr UINode::getAt(const glm::vec2& point, const std::shared_ptr& self) { +std::shared_ptr UINode::getAt(const glm::vec2& point) { if (!isInteractive() || !enabled) { return nullptr; } - return isInside(point) ? self : nullptr; + return isInside(point) ? shared_from_this() : nullptr; } bool UINode::isInteractive() const { diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index 58009e00..beb1bf20 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -63,7 +63,7 @@ namespace gui { }; /// @brief Base abstract class for all UI elements - class UINode { + class UINode : public std::enable_shared_from_this { /// @brief element identifier used for direct access in UiDocument std::string id = ""; /// @brief element enabled state @@ -195,7 +195,7 @@ namespace gui { /// @param pos cursor screen position /// @param self shared pointer to element /// @return self, sub-element or nullptr if element is not interractive - virtual std::shared_ptr getAt(const glm::vec2& pos, const std::shared_ptr& self); + virtual std::shared_ptr getAt(const glm::vec2& pos); /// @brief Check if element is opaque for cursor virtual bool isInteractive() const;