2025-11-23 18:10:06 +03:00

1286 lines
36 KiB
C++

#include "TextBox.hpp"
#include <algorithm>
#include <sstream>
#include <utility>
#include "../GUI.hpp"
#include "../markdown.hpp"
#include "Label.hpp"
#include "assets/Assets.hpp"
#include "devtools/Editor.hpp"
#include "devtools/SyntaxProcessor.hpp"
#include "engine/Engine.hpp"
#include "graphics/core/Batch2D.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Font.hpp"
#include "graphics/ui/markdown.hpp"
#include "util/stringutil.hpp"
#include "window/Window.hpp"
#include "devtools/actions.hpp"
#include "../markdown.hpp"
using namespace gui;
inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40;
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);
reset();
}
void undo() {
sync();
locked = true;
history.undo();
locked = false;
}
void redo() {
sync();
locked = true;
history.redo();
locked = false;
}
void reset() {
pos = -1;
length = 0;
erasing = false;
ss = {};
}
bool isSynced() const {
return length == 0;
}
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(GUI& gui, std::wstring placeholder, glm::vec4 padding)
: Container(gui, glm::vec2(200, 32)),
inputEvents(gui.getInput()),
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);
setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.75f));
label = std::make_shared<Label>(gui, L"");
label->setSize(
size - glm::vec2(padding.z + padding.x, padding.w + padding.y)
);
label->setPos(glm::vec2(
padding.x + LINE_NUMBERS_PANE_WIDTH * showLineNumbers, padding.y
));
add(label);
lineNumbersLabel = std::make_shared<Label>(gui, L"");
lineNumbersLabel->setMultiline(true);
lineNumbersLabel->setSize(
size - glm::vec2(padding.z + padding.x, padding.w + padding.y)
);
lineNumbersLabel->setVerticalAlign(Align::TOP);
add(lineNumbersLabel);
setHoverColor(glm::vec4(0.05f, 0.1f, 0.2f, 0.75f));
textInitX = label->getPos().x;
scrollable = true;
scrollStep = 0;
}
TextBox::~TextBox() = default;
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
Container::draw(pctx, assets);
if (!isFocused() && !keepLineSelection) {
return;
}
const auto& labelText = getText();
glm::vec2 pos = calcPos();
glm::vec2 size = getSize();
auto subctx = pctx.sub();
subctx.setScissors(glm::vec4(pos.x, pos.y, size.x, size.y));
const int lineHeight =
rawTextCache.metrics.lineHeight * label->getLineInterval();
glm::vec2 lcoord = label->calcPos();
lcoord.y -= 2;
auto batch = pctx.getBatch2D();
batch->texture(nullptr);
batch->setColor(glm::vec4(1.0f));
float time = gui.getWindow().time();
if (isFocused() && editable && static_cast<int>((time - caretLastMove) * 2) % 2 == 0) {
uint line = label->getLineByTextIndex(caret);
uint lcaret = caret - label->getTextLineOffset(line);
int width = rawTextCache.metrics.calcWidth(input, 0, lcaret);
batch->rect(
lcoord.x + width,
lcoord.y + label->getLineYOffset(line),
2,
lineHeight
);
}
if (selectionStart != selectionEnd) {
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
uint startLine = label->getLineByTextIndex(selectionStart);
uint endLine = label->getLineByTextIndex(selectionEnd);
batch->setColor(glm::vec4(0.8f, 0.9f, 1.0f, 0.25f));
int start = rawTextCache.metrics.calcWidth(
labelText, 0, selectionStart - label->getTextLineOffset(startLine)
);
int end = rawTextCache.metrics.calcWidth(
labelText, 0, selectionEnd - label->getTextLineOffset(endLine)
);
int lineY = label->getLineYOffset(startLine);
if (startLine == endLine) {
batch->rect(
lcoord.x + start, lcoord.y + lineY, end - start, lineHeight
);
} else {
batch->rect(
lcoord.x + start,
lcoord.y + lineY,
label->getSize().x - start - padding.z - padding.x - 2,
lineHeight
);
for (uint i = startLine + 1; i < endLine; i++) {
batch->rect(
lcoord.x,
lcoord.y + label->getLineYOffset(i),
label->getSize().x - padding.z - padding.x - 2,
lineHeight
);
}
batch->rect(
lcoord.x,
lcoord.y + label->getLineYOffset(endLine),
end,
lineHeight
);
}
}
if (!multiline) {
return;
}
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
batch->setColor(glm::vec4(1, 1, 1, 0.1f));
uint line = label->getLineByTextIndex(caret);
while (label->isFakeLine(line)) {
line--;
}
do {
int lineY = label->getLineYOffset(line);
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
batch->rect(
lcoord.x - 8,
lcoord.y + lineY,
label->getSize().x,
lineHeight
);
batch->setColor(glm::vec4(1, 1, 1, 0.10f));
batch->rect(
lcoord.x - LINE_NUMBERS_PANE_WIDTH,
lcoord.y + lineY,
LINE_NUMBERS_PANE_WIDTH - 8,
lineHeight
);
} else {
batch->rect(
lcoord.x, lcoord.y + lineY, label->getSize().x, lineHeight
);
}
line++;
} while (line < label->getLinesNumber() && label->isFakeLine(line));
}
void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) {
auto font = assets.get<Font>(label->getFontName());
rawTextCache.prepare(
reinterpret_cast<ptrdiff_t>(font),
font->getMetrics(),
label->getSize().x
);
glm::vec2 pos = calcPos();
auto batch = pctx.getBatch2D();
batch->texture(nullptr);
auto subctx = pctx.sub();
subctx.setScissors(glm::vec4(pos.x, pos.y - 0.5, size.x, size.y + 1));
if (valid) {
if (isFocused() && !multiline) {
batch->setColor(focusedColor);
} else if (hover && !multiline) {
batch->setColor(hoverColor);
} else {
batch->setColor(color);
}
} else {
batch->setColor(invalidColor);
}
batch->rect(pos.x, pos.y, size.x, size.y);
if (!isFocused() && supplier) {
input = supplier();
}
refreshLabel();
}
void TextBox::refreshLabel() {
rawTextCache.prepare(
rawTextCache.fontId,
rawTextCache.metrics,
static_cast<size_t>(getSize().x)
);
rawTextCache.update(input, multiline, false);
label->setColor(textColor * glm::vec4(input.empty() ? 0.5f : 1.0f));
const auto& displayText = input.empty() && !hint.empty() ? hint : getText();
if (markup == "md") {
auto [processedText, styles] =
markdown::process(displayText, !focused || !editable);
label->setText(std::move(processedText));
label->setStyles(std::move(styles));
} else {
label->setText(displayText);
if (syntax.empty()) {
label->setStyles(nullptr);
}
}
if (showLineNumbers) {
if (lineNumbersLabel->getLinesNumber() != label->getLinesNumber()) {
std::wstringstream ss;
int n = 1;
for (int i = 1; i <= label->getLinesNumber(); i++) {
if (!label->isFakeLine(i - 1)) {
ss << n;
n++;
}
if (i + 1 <= label->getLinesNumber()) {
ss << "\n";
}
}
lineNumbersLabel->setText(ss.str());
}
lineNumbersLabel->setPos(padding);
lineNumbersLabel->setColor(glm::vec4(1, 1, 1, 0.25f));
}
if (autoresize && rawTextCache.fontId) {
auto size = getSize();
int newy = glm::min(
static_cast<int>(parent->getSize().y),
static_cast<int>(
label->getLinesNumber() * label->getLineInterval() *
rawTextCache.metrics.lineHeight
) + 1
);
if (newy != static_cast<int>(size.y)) {
size.y = newy;
setSize(size);
if (positionfunc) {
pos = positionfunc();
}
}
}
if (multiline && rawTextCache.fontId) {
setScrollable(true);
uint height = label->getLinesNumber() * rawTextCache.metrics.lineHeight *
label->getLineInterval();
label->setSize(glm::vec2(label->getSize().x, height));
actualLength = height;
} else {
setScrollable(false);
}
}
/// @brief Insert text at the caret. Also selected text will be erased
/// @param text Inserting 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 += inputText;
} else {
auto left = input.substr(0, caret);
auto right = input.substr(caret);
input = left + inputText + right;
}
refreshLabel();
setCaret(caret + inputText.length());
if (validate()) {
onInput();
}
}
/// @brief Remove part of the text and move caret to start of the part
/// @param start start of the part
/// @param length length of part that will be removed
void TextBox::erase(size_t start, size_t length) {
size_t end = start + length;
if (caret > start) {
setCaret(caret - length);
}
auto left = input.substr(0, start);
auto right = end >= input.length() ? L"" : input.substr(end);
input = left + right;
}
/// @brief Remove all selected text and reset selection
/// @return true if erased anything
bool TextBox::eraseSelected() {
if (selectionStart == selectionEnd) {
return false;
}
historian->onErase(
selectionStart,
input.substr(selectionStart, selectionEnd - selectionStart),
true
);
erase(selectionStart, selectionEnd - selectionStart);
resetSelection();
onInput();
return true;
}
void TextBox::resetSelection() {
selectionOrigin = 0;
selectionStart = 0;
selectionEnd = 0;
}
void TextBox::extendSelection(int index) {
size_t normalized = normalizeIndex(index);
selectionStart = std::min(selectionOrigin, normalized);
selectionEnd = std::max(selectionOrigin, normalized);
}
size_t TextBox::getLineLength(uint line) const {
size_t position = label->getTextLineOffset(line);
size_t lineLength = label->getTextLineOffset(line + 1) - position;
if (lineLength == 0) {
lineLength = label->getText().length() - position + 1;
}
return lineLength;
}
size_t TextBox::getSelectionLength() const {
return selectionEnd - selectionStart;
}
/// @brief Set scroll offset
/// @param x scroll offset
void TextBox::setTextOffset(uint x) {
textOffset = x;
refresh();
}
void TextBox::typed(unsigned int codepoint) {
if (editable) {
// Combine deleting selected text and inserting a symbol
auto combination = history->beginCombination();
paste(std::wstring({static_cast<wchar_t>(codepoint)}));
}
}
bool TextBox::validate() {
if (validator) {
valid = validator(getText());
} else {
valid = true;
}
return valid;
}
void TextBox::setValid(bool valid) {
this->valid = valid;
}
bool TextBox::isValid() const {
return valid;
}
void TextBox::setMultiline(bool multiline) {
this->multiline = multiline;
label->setMultiline(multiline);
label->setVerticalAlign(multiline ? Align::TOP : Align::CENTER);
}
bool TextBox::isMultiline() const {
return multiline;
}
void TextBox::setTextWrapping(bool flag) {
label->setTextWrapping(flag);
}
bool TextBox::isTextWrapping() const {
return label->isTextWrapping();
}
void TextBox::setEditable(bool editable) {
this->editable = editable;
}
bool TextBox::isEditable() const {
return editable;
}
bool TextBox::isEdited() const {
return history->size() != editedHistorySize || !historian->isSynced();
}
void TextBox::setUnedited() {
historian->sync();
editedHistorySize = history->size();
}
size_t TextBox::getSelectionStart() const {
return selectionStart;
}
size_t TextBox::getSelectionEnd() const {
return selectionEnd;
}
void TextBox::setKeepLineSelection(bool flag) {
keepLineSelection = flag;
}
bool TextBox::isKeepLineSelection() const {
return keepLineSelection;
}
void TextBox::setOnEditStart(runnable oneditstart) {
onEditStart = oneditstart;
}
void TextBox::setAutoResize(bool flag) {
this->autoresize = flag;
}
bool TextBox::isAutoResize() const {
return autoresize;
}
void TextBox::onFocus() {
Container::onFocus();
if (onEditStart) {
setCaret(input.size());
onEditStart();
resetSelection();
}
}
void TextBox::reposition() {
UINode::reposition();
refreshLabel();
}
void TextBox::refresh() {
Container::refresh();
label->setSize(
size - glm::vec2(padding.z + padding.x, padding.w + padding.y)
);
label->setPos(glm::vec2(
padding.x + LINE_NUMBERS_PANE_WIDTH * showLineNumbers + textInitX -
static_cast<int>(textOffset),
padding.y
));
}
/// @brief Clamp index to range [0, input.length()]
/// @param index non-normalized index
/// @return normalized index
size_t TextBox::normalizeIndex(int index) {
return std::min(input.length(), static_cast<size_t>(std::max(0, index)));
}
/// @brief Calculate index of character at defined screen X position
/// @param x screen X position
/// @param y screen Y position
/// @return non-normalized character index
int TextBox::calcIndexAt(int x, int y) const {
if (rawTextCache.fontId == 0) return 0;
const auto& labelText = label->getText();
glm::vec2 lcoord = label->calcPos();
uint line = label->getLineByYOffset(y - lcoord.y);
line = std::min(line, label->getLinesNumber() - 1);
size_t lineLength = getLineLength(line);
uint offset = 0;
while (lcoord.x + rawTextCache.metrics.calcWidth(labelText, 0, offset) < x &&
offset < lineLength - 1) {
offset++;
}
return std::min(
offset + label->getTextLineOffset(line), labelText.length()
);
}
int TextBox::getLineYOffset(int line) const {
if (rawTextCache.fontId == 0) return 0;
return label->getLineYOffset(line);
}
static inline std::wstring get_alphabet(wchar_t c) {
std::wstring alphabet {c};
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
return L"abcdefghijklmnopqrstuvwxyz_"
L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
} else if (c >= '0' && c <= '9') {
return L"0123456789";
}
return alphabet;
}
void TextBox::tokenSelectAt(int index) {
const auto& actualText = label->getText();
if (actualText.empty()) {
return;
}
int left = index;
int right = index;
std::wstring alphabet = get_alphabet(actualText.at(index));
while (left >= 0) {
if (alphabet.find(actualText.at(left)) == std::wstring::npos) {
break;
}
left--;
}
while (static_cast<size_t>(right) < actualText.length()) {
if (alphabet.find(actualText.at(right)) == std::wstring::npos) {
break;
}
right++;
}
select(left + 1, right);
}
void TextBox::doubleClick(int x, int y) {
UINode::doubleClick(x, y);
tokenSelectAt(normalizeIndex(calcIndexAt(x, y) - 1));
}
void TextBox::click(int x, int y) {
int index = normalizeIndex(calcIndexAt(x, y));
selectionStart = index;
selectionEnd = index;
selectionOrigin = index;
}
void TextBox::mouseMove(int x, int y) {
Container::mouseMove(x, y);
if (isScrolling()) {
return;
}
ptrdiff_t index = calcIndexAt(x, y);
setCaret(index);
extendSelection(index);
resetMaxLocalCaret();
}
void TextBox::resetMaxLocalCaret() {
maxLocalCaret =
caret - label->getTextLineOffset(label->getLineByTextIndex(caret));
}
void TextBox::stepLeft(bool shiftPressed, bool breakSelection) {
uint previousCaret = this->caret;
size_t caret = breakSelection ? selectionStart : this->caret;
if (caret > 0) {
if (caret > input.length()) {
setCaret(input.length() - 1);
} else {
setCaret(caret - 1);
}
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(this->caret);
} else {
resetSelection();
}
} else {
setCaret(caret);
resetSelection();
}
resetMaxLocalCaret();
}
void TextBox::stepRight(bool shiftPressed, bool breakSelection) {
uint previousCaret = this->caret;
size_t caret = breakSelection ? selectionEnd : this->caret;
if (caret < input.length()) {
setCaret(caret + 1);
caretLastMove = gui.getWindow().time();
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(this->caret);
} else {
resetSelection();
}
} else {
setCaret(caret);
resetSelection();
}
resetMaxLocalCaret();
}
void TextBox::stepDefaultDown(bool shiftPressed, bool breakSelection) {
uint previousCaret = this->caret;
uint caret = breakSelection ? selectionEnd : this->caret;
uint caretLine = label->getLineByTextIndex(caret);
if (caretLine < label->getLinesNumber() - 1) {
uint offset =
std::min(size_t(maxLocalCaret), getLineLength(caretLine + 1) - 1);
setCaret(label->getTextLineOffset(caretLine + 1) + offset);
} else {
setCaret(input.length());
}
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(this->caret);
} else {
resetSelection();
}
}
void TextBox::stepDefaultUp(bool shiftPressed, bool breakSelection) {
uint previousCaret = this->caret;
uint caret = breakSelection ? selectionStart : this->caret;
uint caretLine = label->getLineByTextIndex(caret);
if (caretLine > 0) {
uint offset =
std::min(size_t(maxLocalCaret), getLineLength(caretLine - 1) - 1);
setCaret(label->getTextLineOffset(caretLine - 1) + offset);
} else {
setCaret(static_cast<size_t>(0));
}
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(this->caret);
} else {
resetSelection();
}
}
static int calc_indent(int linestart, std::wstring_view input) {
int indent = 0;
while (linestart + indent < input.length() &&
input[linestart + indent] == L' ')
indent++;
return indent;
}
void TextBox::onTab(bool shiftPressed) {
std::wstring indentStr = L" ";
if (!shiftPressed && getSelectionLength() == 0) {
paste(indentStr);
return;
}
if (getSelectionLength() == 0) {
selectionStart = caret;
selectionEnd = caret;
selectionOrigin = caret;
}
int lineA = getLineAt(selectionStart);
int lineB = getLineAt(selectionEnd);
int caretLine = getLineAt(caret);
size_t lineAStart = getLinePos(lineA);
size_t lineBStart = getLinePos(lineB);
size_t caretLineStart = getLinePos(caretLine);
size_t caretIndent = calc_indent(caretLineStart, input);
size_t aIndent = calc_indent(lineAStart, input);
size_t bIndent = calc_indent(lineBStart, input);
int lastSelectionStart = selectionStart;
int lastSelectionEnd = selectionEnd;
size_t lastCaret = caret;
auto combination = history->beginCombination();
resetSelection();
for (int line = lineA; line <= lineB; line++) {
size_t linestart = getLinePos(line);
int indent = calc_indent(linestart, input);
if (shiftPressed) {
if (indent >= indentStr.length()) {
setCaret(linestart);
select(linestart, linestart + indentStr.length());
eraseSelected();
}
} else {
setCaret(linestart);
paste(indentStr);
}
refreshLabel(); // todo: replace with textbox cache
}
int linestart = getLinePos(caretLine);
int linestartA = getLinePos(lineA);
int linestartB = getLinePos(lineB);
int la = lastSelectionStart - lineAStart;
int lb = lastSelectionEnd - lineBStart;
if (shiftPressed) {
setCaret(lastCaret - caretLineStart + linestart - std::min<int>(caretIndent, indentStr.length()));
selectionStart = la + linestartA - std::min<int>(std::min<int>(la, aIndent), indentStr.length());
selectionEnd = lb + linestartB - std::min<int>(std::min<int>(lb, bIndent), indentStr.length());
} else {
setCaret(lastCaret - caretLineStart + linestart + indentStr.length());
selectionStart = la + linestartA + indentStr.length();
selectionEnd = lb + linestartB + indentStr.length();
}
if (selectionOrigin == lastSelectionStart) {
selectionOrigin = selectionStart;
} else {
selectionOrigin = selectionEnd;
}
historian->sync();
}
void TextBox::refreshSyntax() {
if (!syntax.empty()) {
const auto& processor = gui.getEditor().getSyntaxProcessor();
auto scheme = gui.getSyntaxColorScheme();
if (auto styles =
processor.highlight(scheme ? *scheme : FontStylesScheme {}, syntax, input)) {
label->setStyles(std::move(styles));
}
}
}
void TextBox::onInput() {
if (subconsumer) {
subconsumer(input);
}
refreshSyntax();
}
void TextBox::performEditingKeyboardEvents(Keycode key) {
bool shiftPressed = gui.getInput().pressed(Keycode::LEFT_SHIFT);
bool breakSelection = getSelectionLength() != 0 && !shiftPressed;
uint current_line = getLineAt(getCaret());
uint previousCaret = getCaret();
if (key == Keycode::BACKSPACE) {
bool erased = eraseSelected();
if (erased) {
if (validate()) {
onInput();
}
} else if (caret > 0 && input.length() > 0) {
if (caret > input.length()) {
caret = input.length();
}
historian->onErase(caret - 1, input.substr(caret - 1, 1));
input = input.substr(0, caret - 1) + input.substr(caret);
setCaret(caret - 1);
if (validate()) {
onInput();
}
}
} else if (key == Keycode::DELETE) {
if (!eraseSelected() && caret < input.length()) {
historian->onErase(caret, input.substr(caret, 1));
input = input.substr(0, caret) + input.substr(caret + 1);
if (validate()) {
onInput();
}
}
} else if (key == Keycode::ENTER) {
if (multiline) {
paste(L"\n");
} else {
defocus();
if (validate() && consumer) {
consumer(getText());
}
}
} else if (key == Keycode::TAB) {
onTab(shiftPressed);
} else if (key == Keycode::LEFT) {
stepLeft(shiftPressed, breakSelection);
} else if (key == Keycode::RIGHT) {
stepRight(shiftPressed, breakSelection);
} else if (key == Keycode::UP && onUpPressed) {
onUpPressed();
} else if (key == Keycode::DOWN && onDownPressed) {
onDownPressed();
} else if (key == Keycode::HOME) {
setCaret(getLinePos(current_line));
resetMaxLocalCaret();
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(getCaret());
} else {
resetSelection();
}
} else if (key == Keycode::END && getLineLength(current_line) > 0) {
setCaret(getLinePos(current_line) + getLineLength(current_line) - 1);
resetMaxLocalCaret();
if (shiftPressed) {
if (selectionStart == selectionEnd) {
selectionOrigin = previousCaret;
}
extendSelection(getCaret());
} else {
resetSelection();
}
}
}
void TextBox::keyPressed(Keycode key) {
const auto& inputEvents = gui.getInput();
if (editable) {
performEditingKeyboardEvents(key);
}
if (inputEvents.pressed(Keycode::LEFT_CONTROL) && key != Keycode::LEFT_CONTROL) {
if (controlCombinationsHandler) {
if (controlCombinationsHandler(static_cast<int>(key))) {
return;
}
}
// Copy selected text to clipboard
if (key == Keycode::C || key == Keycode::X) {
std::string text = util::wstr2str_utf8(getSelection());
if (!text.empty()) {
gui.getInput().setClipboardText(text.c_str());
}
if (editable && key == Keycode::X) {
eraseSelected();
}
}
// Paste text from clipboard
if (key == Keycode::V && editable) {
const char* text = inputEvents.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
if (key == Keycode::A) {
if (selectionStart == selectionEnd) {
select(0, input.length());
} else {
resetSelection();
}
}
if (editable && key == Keycode::Z) {
historian->undo();
refreshSyntax();
}
if (editable && key == Keycode::Y) {
historian->redo();
refreshSyntax();
}
}
}
void TextBox::select(int start, int end) {
if (end < start) {
std::swap(start, end);
}
start = normalizeIndex(start);
end = normalizeIndex(end);
selectionStart = start;
selectionEnd = end;
selectionOrigin = start;
setCaret(selectionEnd);
}
uint TextBox::getLineAt(size_t position) const {
return label->getLineByTextIndex(position);
}
size_t TextBox::getLinePos(uint line) const {
return label->getTextLineOffset(line);
}
std::shared_ptr<UINode> TextBox::getAt(const glm::vec2& pos) {
return UINode::getAt(pos);
}
void TextBox::setOnUpPressed(const runnable& callback) {
if (callback == nullptr) {
onUpPressed = [this]() {
if (inputEvents.pressed(Keycode::LEFT_CONTROL)) {
scrolled(1);
return;
}
bool shiftPressed = inputEvents.pressed(Keycode::LEFT_SHIFT);
bool breakSelection = getSelectionLength() != 0 && !shiftPressed;
stepDefaultUp(shiftPressed, breakSelection);
};
} else {
onUpPressed = callback;
}
}
void TextBox::setOnDownPressed(const runnable& callback) {
if (callback == nullptr) {
onDownPressed = [this]() {
if (inputEvents.pressed(Keycode::LEFT_CONTROL)) {
scrolled(-1);
return;
}
bool shiftPressed = inputEvents.pressed(Keycode::LEFT_SHIFT);
bool breakSelection = getSelectionLength() != 0 && !shiftPressed;
stepDefaultDown(shiftPressed, breakSelection);
};
} else {
onDownPressed = callback;
}
}
void TextBox::setTextSupplier(wstringsupplier supplier) {
this->supplier = std::move(supplier);
}
void TextBox::setTextConsumer(wstringconsumer consumer) {
this->consumer = std::move(consumer);
}
void TextBox::setTextSubConsumer(wstringconsumer consumer) {
this->subconsumer = std::move(consumer);
}
void TextBox::setTextValidator(wstringchecker validator) {
this->validator = std::move(validator);
}
void TextBox::setOnControlCombination(key_handler handler) {
this->controlCombinationsHandler = std::move(handler);
}
void TextBox::setFocusedColor(glm::vec4 color) {
this->focusedColor = color;
}
glm::vec4 TextBox::getFocusedColor() const {
return focusedColor;
}
void TextBox::setTextColor(glm::vec4 color) {
this->textColor = color;
}
glm::vec4 TextBox::getTextColor() const {
return textColor;
}
void TextBox::setErrorColor(glm::vec4 color) {
this->invalidColor = color;
}
glm::vec4 TextBox::getErrorColor() const {
return invalidColor;
}
const std::wstring& TextBox::getText() const {
if (input.empty()) return placeholder;
return input;
}
void TextBox::setText(const std::wstring& value) {
this->input = value;
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
historian->reset();
history->clear();
editedHistorySize = 0;
refreshSyntax();
}
const std::wstring& TextBox::getPlaceholder() const {
return placeholder;
}
void TextBox::setPlaceholder(const std::wstring& placeholder) {
this->placeholder = placeholder;
}
const std::wstring& TextBox::getHint() const {
return hint;
}
void TextBox::setHint(const std::wstring& text) {
this->hint = text;
}
std::wstring TextBox::getSelection() const {
const auto& text = label->getText();
return text.substr(selectionStart, selectionEnd - selectionStart);
}
size_t TextBox::getCaret() const {
return caret;
}
void TextBox::setCaret(size_t position) {
const auto& labelText = label->getText();
caret = std::min(static_cast<size_t>(position), input.length());
if (rawTextCache.fontId == 0) {
return;
}
int width = label->getSize().x;
rawTextCache.prepare(rawTextCache.fontId, rawTextCache.metrics, width);
rawTextCache.update(input, multiline, label->isTextWrapping());
caretLastMove = gui.getWindow().time();
uint line = rawTextCache.getLineByTextIndex(caret);
int offset = label->getLineYOffset(line) + getContentOffset().y;
uint lineHeight = rawTextCache.metrics.lineHeight * label->getLineInterval();
if (scrollStep == 0) {
scrollStep = lineHeight;
}
if (offset < 0) {
scrolled(-glm::floor(offset / static_cast<double>(scrollStep) + 0.5f));
} else if (offset >= getSize().y) {
offset -= getSize().y;
scrolled(-glm::ceil(offset / static_cast<double>(scrollStep) + 0.5f));
}
int lcaret = caret - rawTextCache.getTextLineOffset(line);
int realoffset = rawTextCache.metrics.calcWidth(labelText, 0, lcaret) -
static_cast<int>(textOffset) + 2;
if (realoffset - width > 0) {
setTextOffset(textOffset + realoffset - width);
} else if (realoffset < 0) {
setTextOffset(std::max(textOffset + realoffset, static_cast<size_t>(0)));
}
}
void TextBox::setCaret(ptrdiff_t position) {
if (position < 0) {
setCaret(static_cast<size_t>(input.length() + position + 1));
} else {
setCaret(static_cast<size_t>(position));
}
}
void TextBox::setPadding(glm::vec4 padding) {
this->padding = padding;
refresh();
}
glm::vec4 TextBox::getPadding() const {
return padding;
}
void TextBox::setShowLineNumbers(bool flag) {
showLineNumbers = flag;
}
bool TextBox::isShowLineNumbers() const {
return showLineNumbers;
}
void TextBox::setSyntax(std::string_view lang) {
syntax = lang;
if (syntax.empty()) {
label->setStyles(nullptr);
} else {
refreshSyntax();
}
}
const std::string& TextBox::getSyntax() const {
return syntax;
}
void TextBox::setMarkup(std::string_view lang) {
markup = lang;
}
const std::string& TextBox::getMarkup() const {
return markup;
}