add history to TextBox

This commit is contained in:
MihailRis 2025-03-12 01:52:08 +03:00
parent 223a846784
commit 4c493aff25
7 changed files with 230 additions and 33 deletions

View File

@ -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);
}

View File

@ -17,9 +17,7 @@ Container::~Container() {
Container::clear();
}
std::shared_ptr<UINode> Container::getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) {
std::shared_ptr<UINode> Container::getAt(const glm::vec2& pos) {
if (!isInteractive() || !isEnabled()) {
return nullptr;
}
@ -28,19 +26,19 @@ std::shared_ptr<UINode> 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) {

View File

@ -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<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self) override;
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
virtual void add(const std::shared_ptr<UINode>& node);
virtual void add(const std::shared_ptr<UINode>& node, glm::vec2 pos);
virtual void clear();

View File

@ -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> textbox;
size_t position;
std::wstring string;
public:
InputAction(
std::weak_ptr<TextBox> 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> textbox;
size_t start;
size_t end;
public:
SelectionAction(std::weak_ptr<TextBox> 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<size_t>(-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<SelectionAction>(
getTextBoxWeakptr(),
textBox.getSelectionStart(),
textBox.getSelectionEnd()
),
true
);
}
if (this->pos == static_cast<size_t>(-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<InputAction>(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<TextBox> getTextBoxWeakptr() {
return std::weak_ptr<TextBox>(std::dynamic_pointer_cast<TextBox>(
textBox.shared_from_this()
));
}
};
}
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
: Container(glm::vec2(200, 32)),
history(std::make_shared<ActionsHistory>()),
historian(std::make_unique<TextBoxHistorian>(*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<wchar_t>(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<UINode> TextBox::getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) {
return UINode::getAt(pos, self);
std::shared_ptr<UINode> TextBox::getAt(const glm::vec2& pos) {
return UINode::getAt(pos);
}
void TextBox::setOnUpPressed(const runnable &callback) {

View File

@ -4,10 +4,14 @@
#include "Label.hpp"
class Font;
class ActionsHistory;
namespace gui {
class TextBoxHistorian;
class TextBox : public Container {
LabelCache rawTextCache;
std::shared_ptr<ActionsHistory> history;
std::unique_ptr<TextBoxHistorian> 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<UINode> getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) override;
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
virtual void setOnUpPressed(const runnable &callback);
virtual void setOnDownPressed(const runnable &callback);

View File

@ -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> UINode::getAt(const glm::vec2& point, const std::shared_ptr<UINode>& self) {
std::shared_ptr<UINode> 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 {

View File

@ -63,7 +63,7 @@ namespace gui {
};
/// @brief Base abstract class for all UI elements
class UINode {
class UINode : public std::enable_shared_from_this<UINode> {
/// @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<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self);
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos);
/// @brief Check if element is opaque for cursor
virtual bool isInteractive() const;