1286 lines
36 KiB
C++
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;
|
|
}
|