Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /// hpp
- #pragma once
- #include "Graphics/Color.hpp"
- #include "UI/Widget.hpp"
- #include "UI/fwd.hpp"
- #include "Utils/Sigslot.hpp"
- #include "Utils/UndoRedo.hpp"
- #include <GL/gl3w.h>
- #include <memory>
- #include <span>
- #include <string>
- #include <string_view>
- #include <vector>
- struct CharLocation {
- int line;
- int column;
- std::string Format() const;
- CharLocation Next(int chars = 1) const;
- CharLocation Prev(int chars = 1) const;
- CharLocation NextLine(int lines = 1) const;
- CharLocation PrevLine(int lines = 1) const;
- CharLocation Downwards(int lines = 1) const;
- CharLocation Upwards(int lines = 1) const;
- auto operator<=>(const CharLocation&) const = default;
- };
- /// [begin, end) for chars (column)
- /// [begin, end] for lines
- /// Example: (10,0) (10,5) includes a 5 char sequence
- /// Example: (10,0) (11,0) with line 10 being 5 chars long, includes a 6 char sequence
- /// with the last one being the linebreak character
- struct TextSelection {
- CharLocation begin;
- CharLocation end;
- int GetHorizontalLength() const;
- int GetVerticalLength() const;
- std::string Format() const;
- std::string FormatFull() const;
- };
- /// A text editor that only concerns about characters, a single caret, no syntax highlighting, and no folding.
- class TextEdit : public Widget {
- private:
- /// A list of actions belonging to this TextEdit. This will be used regardless of whether a root
- /// (and its UndoStack) is attached.
- std::vector<std::shared_ptr<IAction>> mActions;
- struct Caret {
- /// Index of the line that the caret is on.
- int line = 0;
- /// The "desired" column that the caret wants to be. Clamped with current line length
- /// to get the actual column.
- /// Unit: glyphs
- int desiredColumn = 0;
- /// Unit: glyphs
- int column = 0;
- /// Unit: UTF-8 code units (bytes)
- int byteOffset = 0;
- } mCaret;
- /// "Selection Anchor"
- CharLocation mSA{ -1, -1 };
- /// Using a shared_ptr allows this to interface with other text storages (such as code blocks)
- /// without maintining 2 copies of the data and the consistency between them.
- std::shared_ptr<std::vector<std::string>> mDocument;
- Font* mFont;
- RgbaColor mTextColor;
- /// The index of the first drawn line.
- int mLineOffset;
- /// Cache for how many glyphs are currently present in this document. Used for buffer allocation.
- int mGlyphCount;
- GLuint mGlyphVertexArray;
- GLuint mGlyphVBO;
- GLuint mGlyphIBO;
- GLsizei mIndices;
- GLuint mCaretVertexArray;
- GLuint mCaretVBO;
- bool mNeedsRedraw = false;
- public:
- TextEdit();
- TextEdit(const std::shared_ptr<std::vector<std::string>>& doc);
- virtual ~TextEdit();
- virtual void PostDrawCall() final override;
- void RebuildBuffers();
- virtual void MousePressed(MouseEvent& event) override;
- virtual void KeyPressed(KeyEvent& event) override;
- virtual void CharacterInput(CharacterInputEvent& event) override;
- Font& GetFont() const;
- void SetFont(Font& font);
- RgbaColor GetTextColor() const;
- void SetTextColor(RgbaColor color);
- const std::vector<std::string>& GetDocument() const;
- void SetDocument(const std::shared_ptr<std::vector<std::string>>& doc);
- void SetDocument(std::vector<std::string> doc);
- CharLocation GetCaret() const;
- void SetCaretLine(int line);
- void SetCaretColumn(int column);
- void ScrollToFront();
- void ScrollToBack();
- /// \see InsertText(CharLocation, int, std::string_view)
- bool InsertText(CharLocation loc, std::string_view text);
- /// Insert the given text at the given location, and use `byteOffset` to split the line.
- /// All line breaks ('\n') will be processed and generate corresponding separate lines.
- ///
- /// This method is unsafe: no bound or UTF-8 alignment checks are performed. Use with caution.
- /// (And therefore a success flag is not provided).
- void InsertText(CharLocation loc, int byteOffset, std::string_view text);
- /// Strictly inserts text after the caret, ignoring the selection. Use replaceSelection() if you
- /// want to emulate typing the given text.
- /// This method is gaurenteed to not fail, so a success flag is not provided.
- void InsertTextAtCaret(std::string_view text);
- /// Insert the given `text` into the document, so that the text forms a line with index `line`.
- /// Extra empty lines are inserted if `lien` is outside of the current document range.
- void InsertLine(int line, std::string text);
- void AppendLine(std::string text);
- /// Insert a line break at the given location, and everything after the location on the line will
- /// be moved to a new line.
- bool InsertLineBreak(CharLocation loc);
- bool RemoveText(TextSelection range);
- bool RemoveLine(int line);
- /// Move the range of lines [begin, end) up or down by `distance`. The direction of movement is
- /// determined by the sign of `distance`, where positive indicates down and negative indicates up.
- bool MoveLines(int begin, int end, int distance);
- /// Move the selection by the `distance`. When there is no selection, caret's current line
- /// is considered the "selection".
- ///
- /// \see moveSelection(int, int, int)
- bool MoveSelection(int distance);
- bool HasSelection() const;
- TextSelection GetSelection() const;
- void SetSelection(TextSelection selection, bool caretAtEnd = false);
- void ClearSelection();
- std::string GetSelectedText() const;
- std::vector<std::string> GetSelectedLines();
- /// Replace the selection if present, otherwise delegate to insertTextAtCaret().
- bool ReplaceSelectionOrInsert(std::string_view text);
- bool ReplaceSelection(std::string_view text);
- bool RemoveSelection();
- void CutToClipboard();
- void CopyToClipboard();
- void PasteFromClipboard();
- void DeleteLine();
- };
- /// cpp
- #define NOMINMAX
- #include "TextEdit.hpp"
- #include "Graphics/Font.hpp"
- #include "Graphics/Painter.hpp"
- #include "Graphics/Vertex.hpp"
- #include "UI/Input.hpp"
- #include "UI/RootWidget.hpp"
- #include "Utils/String.hpp"
- #include <GL/gl3w.h>
- #include <GLFW/glfw3.h>
- #include <doctest/doctest.h>
- #include <algorithm>
- #include <memory>
- #include <utility>
- #ifndef DOCTEST_CONFIG_DISABLE
- # include "UI/RootWidget.hpp"
- #endif
- std::string CharLocation::Format() const {
- return std::to_string(line + 1) + ":" + std::to_string(column + 1);
- }
- CharLocation CharLocation::Next(int chars) const {
- return { line, column + chars };
- }
- CharLocation CharLocation::Prev(int chars) const {
- return { line, column - chars };
- }
- CharLocation CharLocation::NextLine(int lines) const {
- return { line + lines, 0 };
- }
- CharLocation CharLocation::PrevLine(int lines) const {
- return { line - lines, 0 };
- }
- CharLocation CharLocation::Downwards(int lines) const {
- return { line + lines, column };
- }
- CharLocation CharLocation::Upwards(int lines) const {
- return { line - lines, column };
- }
- TEST_CASE("CharLocation line difference") {
- CharLocation l1{ 0, 0 };
- CharLocation l2{ 3, 0 };
- CHECK(l1 < l2);
- }
- TEST_CASE("CharLocation column difference") {
- CharLocation l1{ 2, 4 };
- CharLocation l2{ 2, 8 };
- CHECK(l1 < l2);
- }
- TEST_CASE("CharLocation line & column difference") {
- CharLocation l1{ 0, 12 };
- CharLocation l2{ 2, 7 };
- CHECK(l1 < l2);
- }
- int TextSelection::GetHorizontalLength() const {
- return std::abs(end.column - begin.column);
- }
- int TextSelection::GetVerticalLength() const {
- return end.line - begin.line + 1;
- }
- std::string TextSelection::Format() const {
- return begin.Format();
- }
- std::string TextSelection::FormatFull() const {
- return "[" + begin.Format() + ", " + end.Format() + "]";
- }
- namespace {
- class TextManipulationAction : public IAction {
- protected:
- TextEdit* mTextEdit;
- std::string mText;
- TextSelection mRange;
- public:
- TextManipulationAction(TextEdit& widget, std::string text, const TextSelection& range)
- : mTextEdit{ &widget }
- , mText{ std::move(text) }
- , mRange{ range } {
- }
- };
- class InsertAction : public TextManipulationAction {
- public:
- virtual void DoAction() override {
- mTextEdit->InsertText(mRange.begin, mText);
- }
- virtual void UndoAction() override {
- mTextEdit->RemoveText(mRange);
- }
- };
- class DeleteAction : public TextManipulationAction {
- public:
- virtual void DoAction() override {
- mTextEdit->RemoveText(mRange);
- }
- virtual void UndoAction() override {
- mTextEdit->InsertText(mRange.begin, mText);
- }
- };
- } // namespace
- TextEdit::TextEdit()
- : TextEdit(std::make_shared<std::vector<std::string>>()) {
- }
- TextEdit::TextEdit(const std::shared_ptr<std::vector<std::string>>& doc)
- : mDocument{ doc }
- , mFont{ Font::sans } {
- #ifdef DOCTEST_CONFIG_DISABLE
- glGenVertexArrays(1, &mGlyphVertexArray);
- glGenBuffers(1, &mGlyphVBO);
- glGenBuffers(1, &mGlyphIBO);
- SetHasSpecialDrawRoutine(true);
- #else
- // Do nothing
- #endif
- }
- TextEdit::~TextEdit() {
- #ifdef DOCTEST_CONFIG_DISABLE
- glDeleteBuffers(1, &mGlyphVertexArray);
- glDeleteBuffers(1, &mGlyphVBO);
- glDeleteBuffers(1, &mGlyphIBO);
- #else
- // Do nothing
- #endif
- }
- void TextEdit::PostDrawCall() {
- #ifdef DOCTEST_CONFIG_DISABLE
- if (mNeedsRedraw) {
- RebuildBuffers();
- }
- glBindVertexArray(mGlyphVertexArray);
- glDrawElements(GL_TRIANGLES, mIndices, GL_UNSIGNED_SHORT, (void*)0);
- #else
- // Do nothing
- #endif
- }
- void TextEdit::RebuildBuffers() {
- #ifdef DOCTEST_CONFIG_DISABLE
- auto textVertices = std::make_unique<TexturedVertex[]>(mGlyphCount * 4);
- auto textIndices = std::make_unique<uint16_t[]>(mGlyphCount * 6);
- Font::DrawTargetPointer target{
- .vertices = textVertices.get(),
- .indices = textIndices.get(),
- .initialVertexIdx = 0,
- .pos = glm::vec3{ GetGlobalGeometry().TopLeft(), GetDepth() },
- .color = mTextColor,
- };
- for (auto& line : *mDocument) {
- mFont->DrawTo(line, target);
- target.vertices += 4;
- target.indices += 6;
- target.initialVertexIdx += 4;
- target.pos.y += mFont->GetFontHeight();
- }
- glBindBuffer(GL_ARRAY_BUFFER, mGlyphVBO);
- glBufferData(GL_ARRAY_BUFFER, mGlyphCount * 4 * sizeof(TexturedVertex), textVertices.get(), GL_STATIC_DRAW);
- glBindBuffer(GL_ARRAY_BUFFER, mGlyphIBO);
- glBufferData(GL_ARRAY_BUFFER, mGlyphCount * 6 * sizeof(uint16_t), textIndices.get(), GL_STATIC_DRAW);
- ColoredVertex caretVertices[6];
- // TODO calc caret pos
- // TODO draw caret
- glBindBuffer(GL_ARRAY_BUFFER, mCaretVBO);
- glBufferData(GL_ARRAY_BUFFER, 2 * 3 * sizeof(ColoredVertex), caretVertices, GL_DYNAMIC_DRAW);
- mNeedsRedraw = false;
- #else
- // Do nothing, we don't want the tests to cover OpenGL stuff (which aren't going to be present in the testing environment, causing crashes)
- #endif
- }
- void TextEdit::MousePressed(MouseEvent& event) {
- if (event.IsConsumed()) {
- return;
- }
- // TODO
- }
- void TextEdit::KeyPressed(KeyEvent& event) {
- if (event.IsConsumed()) {
- return;
- }
- auto UpdateSelectionAnchor = [&]() {
- if (event.HasMod(GLFW_MOD_SHIFT)) {
- if (!HasSelection()) {
- mSA.line = mCaret.line;
- mSA.column = mCaret.column;
- }
- } else {
- // Clear selection regardless of whether the caret moved or not
- ClearSelection();
- }
- };
- switch (event.key) {
- case GLFW_KEY_LEFT:
- case GLFW_KEY_RIGHT: {
- UpdateSelectionAnchor();
- std::string_view line{ (*mDocument)[mCaret.line] };
- if (mCaret.byteOffset == 0 && event.key == GLFW_KEY_LEFT) {
- // Start of the document, can't move left anymore
- if (mCaret.line == 0) {
- ClearSelection(); // Clear selection in case we reach the GLFW_MOD_SHIFT && hasSelection() branch
- break;
- }
- mCaret.line--;
- line = (*mDocument)[mCaret.line];
- // Last character in previous line, i.e. the '\n'
- auto [index, byteOffset] = StringLastCodepoint(line);
- mCaret.column = index;
- mCaret.desiredColumn = index;
- mCaret.byteOffset = byteOffset;
- } else if (mCaret.byteOffset == line.length() - 1 && event.key == GLFW_KEY_RIGHT) {
- // End of the document, can't move right anymore
- if (mCaret.line == mDocument->size() - 1) {
- ClearSelection();
- break;
- }
- mCaret.line++;
- line = (*mDocument)[mCaret.line];
- mCaret.column = 0;
- mCaret.desiredColumn = 0;
- mCaret.byteOffset = 0;
- } else {
- int col = mCaret.column;
- Utf8Iterator it{ line.begin() + mCaret.byteOffset };
- if (event.key == GLFW_KEY_LEFT) {
- col -= 1;
- it--;
- } else {
- col += 1;
- it++;
- }
- mCaret.column = col;
- mCaret.desiredColumn = col;
- mCaret.byteOffset = std::distance(line.begin(), it.AsInternal());
- }
- // Normal exit condition: successfull
- event.Consume();
- } break;
- case GLFW_KEY_DOWN: {
- constexpr Modifiers kMask = GLFW_MOD_CONTROL | GLFW_MOD_SHIFT;
- if ((event.modifiers & kMask) == kMask) {
- MoveSelection(1);
- break;
- }
- UpdateSelectionAnchor();
- if (mCaret.line == mDocument->size() - 1) {
- ClearSelection();
- break;
- }
- SetCaretLine(mCaret.line + 1);
- event.Consume();
- } break;
- case GLFW_KEY_UP: {
- constexpr Modifiers kMask = GLFW_MOD_CONTROL | GLFW_MOD_SHIFT;
- if ((event.modifiers & kMask) == kMask) {
- MoveSelection(-1);
- break;
- }
- UpdateSelectionAnchor();
- if (mCaret.line == 0) {
- ClearSelection();
- break;
- }
- SetCaretLine(mCaret.line - 1);
- event.Consume();
- } break;
- case GLFW_KEY_HOME: {
- UpdateSelectionAnchor();
- ScrollToFront();
- event.Consume();
- break;
- }
- case GLFW_KEY_END: {
- UpdateSelectionAnchor();
- ScrollToBack();
- event.Consume();
- break;
- }
- case GLFW_KEY_ENTER: {
- auto loc = GetCaret();
- if (HasSelection()) {
- RemoveSelection();
- }
- InsertLineBreak(loc);
- event.Consume();
- break;
- }
- case GLFW_KEY_BACKSPACE: {
- if (HasSelection()) {
- RemoveText(GetSelection());
- } else if (mCaret.column == 0) {
- if (mCaret.line > 0) {
- auto& prevLine = (*mDocument)[mCaret.line - 1];
- auto& line = (*mDocument)[mCaret.line];
- prevLine.pop_back(); // Remove '\n'
- prevLine += line;
- RemoveLine(mCaret.line);
- mNeedsRedraw = true;
- }
- } else {
- auto& line = (*mDocument)[mCaret.line];
- std::string_view sv{ line };
- Utf8Iterator it{ sv.begin() + mCaret.byteOffset };
- it--;
- auto leftOffset = std::distance(sv.begin(), it.AsInternal());
- line = line.substr(0, leftOffset); // Everything before 1 before caret
- line += line.substr(mCaret.byteOffset); // Everything after (inclusive) caret
- mNeedsRedraw = true;
- }
- event.Consume();
- break;
- }
- case GLFW_KEY_DELETE: {
- if (HasSelection()) {
- RemoveText(GetSelection());
- } else if (mCaret.byteOffset == (*mDocument)[mCaret.line].length()) {
- if (mCaret.line < mDocument->size() - 1) {
- auto& line = (*mDocument)[mCaret.line];
- auto& nextLine = (*mDocument)[mCaret.line + 1];
- line.pop_back(); // Remove '\n'
- line += nextLine;
- RemoveLine(mCaret.line + 1);
- mNeedsRedraw = true;
- }
- } else {
- auto& line = (*mDocument)[mCaret.line];
- std::string_view sv{ line };
- Utf8Iterator it{ sv.begin() + mCaret.byteOffset };
- it++;
- auto rightOffset = std::distance(sv.begin(), it.AsInternal());
- line = line.substr(0, mCaret.byteOffset); // Everything before caret
- line += line.substr(rightOffset); // Everything after (exclusive) caret
- mNeedsRedraw = true;
- }
- event.Consume();
- break;
- }
- case GLFW_KEY_X: {
- if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
- event.Consume();
- CutToClipboard();
- }
- } break;
- case GLFW_KEY_C: {
- if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
- event.Consume();
- CopyToClipboard();
- }
- } break;
- case GLFW_KEY_V: {
- if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
- event.Consume();
- PasteFromClipboard();
- }
- } break;
- case GLFW_KEY_Y: {
- if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
- event.Consume();
- DeleteLine();
- }
- } break;
- }
- }
- void TextEdit::CharacterInput(CharacterInputEvent& event) {
- std::u32string str;
- str += event.character;
- InsertTextAtCaret(ConvertUtf32To8(str));
- }
- Font& TextEdit::GetFont() const {
- return *mFont;
- }
- void TextEdit::SetFont(Font& font) {
- mFont = &font;
- mNeedsRedraw = true;
- }
- RgbaColor TextEdit::GetTextColor() const {
- return mTextColor;
- }
- void TextEdit::SetTextColor(RgbaColor color) {
- mTextColor = color;
- mNeedsRedraw = true;
- }
- const std::vector<std::string>& TextEdit::GetDocument() const {
- return *mDocument;
- }
- void TextEdit::SetDocument(const std::shared_ptr<std::vector<std::string>>& doc) {
- mDocument = doc;
- mActions.clear();
- mNeedsRedraw = true;
- }
- void TextEdit::SetDocument(std::vector<std::string> doc) {
- mDocument = std::make_shared<std::vector<std::string>>(std::move(doc));
- mActions.clear();
- mNeedsRedraw = true;
- }
- CharLocation TextEdit::GetCaret() const {
- return { mCaret.line, mCaret.column };
- }
- void TextEdit::SetCaretLine(int lineNumber) {
- mCaret.line = std::clamp(lineNumber, 0, (int)mDocument->size() - 1);
- auto& line = (*mDocument)[mCaret.line];
- auto [clamped, byteOffset] = StringCodepoint(line, mCaret.desiredColumn);
- mCaret.column = clamped;
- mCaret.byteOffset = byteOffset;
- }
- void TextEdit::SetCaretColumn(int column) {
- mCaret.desiredColumn = column;
- auto& line = (*mDocument)[mCaret.line];
- auto [clamped, byteOffset] = StringCodepoint(line, column);
- mCaret.column = clamped;
- mCaret.byteOffset = byteOffset;
- }
- void TextEdit::ScrollToFront() {
- mCaret.column = 0;
- mCaret.desiredColumn = 0;
- mCaret.byteOffset = 0;
- }
- void TextEdit::ScrollToBack() {
- auto& line = (*mDocument)[mCaret.line];
- auto [index, byteOfset] = StringLastCodepoint(line);
- mCaret.column = index;
- mCaret.desiredColumn = index;
- mCaret.byteOffset = byteOfset;
- }
- bool TextEdit::InsertText(CharLocation loc, std::string_view text) {
- if (loc.line < 0 || loc.line >= mDocument->size()) {
- return false;
- }
- auto& line = (*mDocument)[loc.line];
- auto [clamped, byteOffset] = StringCodepoint(line, loc.column);
- loc.column = clamped;
- InsertText(loc, byteOffset, text);
- return true;
- }
- void TextEdit::InsertText(CharLocation loc, int byteOffset, std::string_view text) {
- std::vector<std::string> textLines;
- std::string buf;
- for (char c : text) {
- if (c == '\n') {
- textLines.push_back(std::move(buf));
- buf.clear();
- } else {
- buf += c;
- }
- }
- if (!buf.empty()) {
- textLines.push_back(std::move(buf));
- }
- auto& firstLine = (*mDocument)[loc.line];
- auto front = firstLine.substr(0, byteOffset);
- auto back = firstLine.substr(byteOffset);
- if (textLines[0].back() == '\n') {
- firstLine = front + textLines[0];
- if (textLines.size() >= 2) {
- mDocument->insert(
- mDocument->begin() + loc.line,
- std::make_move_iterator(textLines.begin() + 1),
- std::make_move_iterator(textLines.end()));
- if (textLines.back().back() == '\n') {
- // Last line has a line break, `back` should be inserted as a separate line
- mDocument->insert(
- mDocument->begin() + loc.line + textLines.size(),
- back);
- } else {
- // `back` should be inserted after the last line
- (*mDocument)[loc.line + textLines.size()] += back;
- }
- }
- } else {
- // `buf` isn't empty: there was no \n, it's going to be stuffed between `front` and `back`
- firstLine = front + textLines[0] + back;
- }
- // TODO handle caret positioning
- }
- void TextEdit::InsertTextAtCaret(std::string_view text) {
- InsertText(GetCaret(), text);
- }
- void TextEdit::InsertLine(int line, std::string text) {
- if (text.back() != '\n') {
- text += '\n';
- }
- if (line < 0) {
- std::vector<std::string> cand(-line + 1);
- cand[0] = std::move(text);
- mDocument->insert(mDocument->begin(), cand.begin(), cand.end());
- } else {
- mDocument->insert(mDocument->begin() + line, std::move(text));
- }
- if (mCaret.line > line) {
- mCaret.line--;
- }
- if (HasSelection() && mSA.line > line) {
- mSA.line--;
- }
- mNeedsRedraw = true;
- }
- void TextEdit::AppendLine(std::string text) {
- if (text.back() != '\n') {
- text.push_back('\n');
- }
- mDocument->push_back(std::move(text));
- mNeedsRedraw = true;
- }
- bool TextEdit::InsertLineBreak(CharLocation loc) {
- if (loc.line < 0 || loc.line >= mDocument->size()) return false;
- auto& line = (*mDocument)[loc.line];
- auto [_, byteOffset] = StringCodepoint(line, loc.column);
- auto front = line.substr(0, byteOffset);
- auto back = line.substr(byteOffset);
- line = front + "\n";
- InsertLine(loc.line + 1, back);
- return true;
- }
- bool TextEdit::RemoveText(TextSelection range) {
- if (range.begin.line < 0 || range.begin.line >= mDocument->size()) return false;
- if (range.end.line < 0 || range.end.line >= mDocument->size()) return false;
- if (range.begin.line > range.end.line) return false;
- if (range.begin.line == range.end.line) {
- auto& line = (*mDocument)[range.begin.line];
- auto begin = StringCodepoint(line, range.begin.column).byteOffset;
- auto end = StringCodepoint(line, range.end.column).byteOffset;
- auto front = line.substr(0, begin);
- auto back = line.substr(end);
- line = front + back;
- } else {
- auto& first = (*mDocument)[range.begin.line];
- first.resize(StringCodepoint(first, range.begin.column).byteOffset);
- // More than 2 lines total, i.e. have middle parts
- if (range.end.line - range.begin.line + 1 >= 2) {
- auto begin = mDocument->begin() + range.begin.line + 1;
- auto end = mDocument->end() + range.end.line;
- mDocument->erase(begin, end);
- }
- auto& last = (*mDocument)[range.end.line];
- last = last.substr(StringCodepoint(last, range.end.column).byteOffset);
- }
- return true;
- }
- bool TextEdit::RemoveLine(int line) {
- if (line < 0 || line >= mDocument->size()) {
- return false;
- }
- mDocument->erase(mDocument->begin() + line);
- // Don't need to recalculate column: we are still on the same line (contents), it's just line number changed
- if (mCaret.line > line) {
- mCaret.line--;
- } else if (mCaret.line == line) {
- if (line == mDocument->size() - 1) {
- mDocument->push_back("\n");
- mCaret.column = 0;
- mCaret.byteOffset = 0;
- } else {
- SetCaretLine(mCaret.line);
- }
- }
- if (HasSelection()) {
- if (mSA.line > line) {
- mSA.line--;
- } else if (mSA.line == line) {
- auto& line = (*mDocument)[mSA.line];
- auto [column, _] = StringCodepoint(line, mSA.column);
- mSA.column = column;
- }
- }
- mNeedsRedraw = true;
- return true;
- }
- bool TextEdit::MoveLines(int begin, int end, int distance) {
- if (distance > 0) {
- // Affects range [begin, end + |distance|)
- if (begin < 0) return false;
- if (end + distance >= mDocument->size()) return false;
- // Make the non-"selection" the first in range
- std::rotate(
- mDocument->begin() + begin,
- mDocument->begin() + end,
- mDocument->begin() + end + distance);
- } else {
- distance = -distance; // Make positive
- // Affects range [begin - |distance|, end)
- if (begin - distance < 0) return false;
- if (end >= mDocument->size()) return false;
- // Make "selection" the first in range
- std::rotate(
- mDocument->begin() + begin - distance,
- mDocument->begin() + begin,
- mDocument->begin() + end);
- }
- return true;
- }
- bool TextEdit::MoveSelection(int distance) {
- if (HasSelection()) {
- auto sel = GetSelection();
- return MoveLines(sel.begin.line, sel.end.line + 1, distance);
- } else {
- return MoveLines(mCaret.line, mCaret.line + 1, distance);
- }
- }
- bool TextEdit::HasSelection() const {
- return mSA.line != -1 && mSA.column != -1;
- }
- TextSelection TextEdit::GetSelection() const {
- return TextSelection{
- .begin = std::min(GetCaret(), mSA),
- .end = std::max(GetCaret(), mSA),
- };
- }
- void TextEdit::SetSelection(TextSelection selection, bool caretAtEnd) {
- selection.begin.line = std::clamp(selection.begin.line, 0, (int)mDocument->size() - 1);
- selection.end.line = std::clamp(selection.end.line, 0, (int)mDocument->size() - 1);
- auto assign = [&](CharLocation sa, CharLocation caret) {
- mSA.line = sa.line;
- mSA.column = sa.column;
- mCaret.line = caret.line;
- mCaret.column = caret.column;
- mCaret.desiredColumn = caret.column;
- };
- if (caretAtEnd) {
- assign(selection.begin, selection.end);
- } else {
- assign(selection.end, selection.begin);
- }
- mCaret.byteOffset = StringCodepoint((*mDocument)[mCaret.line], mCaret.column).byteOffset;
- }
- void TextEdit::ClearSelection() {
- mSA.line = -1;
- mSA.column = -1;
- }
- std::string TextEdit::GetSelectedText() const {
- auto sel = GetSelection();
- auto& lines = *mDocument;
- std::string res;
- if (sel.begin.line == sel.end.line) {
- std::string_view line{ lines[sel.begin.line] };
- res.append(StringRange(line, sel.begin.column, sel.end.column));
- } else {
- res.append(std::string_view(lines[sel.begin.line]).substr(sel.begin.column));
- for (int i = sel.begin.line + 1; i < sel.end.line; ++i) {
- res.append(lines[i]);
- }
- res.append(std::string_view(lines[sel.end.line]).substr(0, sel.end.column));
- }
- return res;
- }
- std::vector<std::string> TextEdit::GetSelectedLines() {
- auto sel = GetSelection();
- auto& lines = *mDocument;
- std::vector<std::string> res;
- res.reserve(sel.end.line - sel.begin.line + 1);
- if (sel.begin.line == sel.end.line) {
- std::string_view line{ lines[sel.begin.line] };
- std::string range(StringRange(line, sel.begin.column, sel.end.column));
- res.push_back(std::move(range));
- } else {
- res.push_back(lines[sel.begin.line].substr(sel.begin.column));
- for (int i = sel.begin.line + 1; i < sel.end.line; ++i) {
- res.push_back(lines[i]);
- }
- res.push_back(lines[sel.end.line].substr(0, sel.end.column));
- }
- return res;
- }
- bool TextEdit::ReplaceSelectionOrInsert(std::string_view text) {
- if (HasSelection()) {
- return ReplaceSelection(text);
- } else {
- InsertTextAtCaret(text);
- return true;
- }
- }
- bool TextEdit::ReplaceSelection(std::string_view text) {
- if (HasSelection()) {
- auto sel = GetSelection();
- RemoveText(sel);
- InsertText(sel.begin, text);
- return true;
- }
- return false;
- }
- bool TextEdit::RemoveSelection() {
- if (HasSelection()) {
- RemoveText(GetSelection());
- return true;
- }
- return false;
- }
- void TextEdit::CutToClipboard() {
- if (HasSelection()) {
- if (mRoot) {
- auto text = GetSelectedText();
- mRoot->GetIO().SetClipboardText(text.c_str());
- }
- RemoveSelection();
- } else {
- if (mRoot) {
- auto& text = (*mDocument)[mCaret.line];
- mRoot->GetIO().SetClipboardText(text.c_str());
- }
- RemoveLine(mCaret.line);
- }
- }
- void TextEdit::CopyToClipboard() {
- if (!mRoot) return;
- if (HasSelection()) {
- auto text = GetSelectedText();
- mRoot->GetIO().SetClipboardText(text.c_str());
- } else {
- auto text = (*mDocument)[mCaret.line];
- mRoot->GetIO().SetClipboardText(text.c_str());
- }
- }
- void TextEdit::PasteFromClipboard() {
- if (!mRoot) return;
- auto text = mRoot->GetIO().GetClipboardText();
- if (HasSelection()) {
- ReplaceSelection(text);
- } else {
- InsertTextAtCaret(text);
- }
- }
- void TextEdit::DeleteLine() {
- ClearSelection();
- RemoveLine(mCaret.line);
- }
- TEST_CASE("TextEdit tests") {
- InputState io(nullptr);
- RootWidget root(io);
- TextEdit te;
- root.AddWidget(&te);
- auto AcceptEvent = [&](int key, int modifiers = 0) {
- KeyEvent event{
- .key = key,
- .scancode = -1, // Doesn't matter
- .modifiers = modifiers,
- };
- te.KeyPressed(event);
- };
- SUBCASE("Inserting lines") {
- te.InsertLine(0, "This is a test line");
- te.InsertLine(0, "This is another test line");
- CHECK(te.GetDocument()[0] == "This is another test line\n");
- CHECK(te.GetDocument()[1] == "This is a test line\n");
- }
- SUBCASE("Removing lines") {
- te.AppendLine("This is a test line");
- te.AppendLine("<junk>");
- te.AppendLine("This is another test line");
- CHECK(te.GetDocument()[1] == "<junk>\n");
- te.RemoveLine(1);
- CHECK(te.GetDocument().size() == 2);
- CHECK(te.GetDocument()[1] == "This is another test line\n");
- }
- SUBCASE("Inserting ranges") {
- te.AppendLine("This is a test line");
- te.AppendLine("This is another test line");
- te.InsertText(CharLocation{ 0, 5 }, "<some extra stuff>");
- CHECK(te.GetDocument()[0] == "This <some extra stuff>is a test line\n");
- }
- SUBCASE("Inserting multi-line ranges") {
- te.AppendLine("This is a test line");
- SUBCASE("No line break at the end") {
- te.InsertText({ 0, 3 }, "<some extra stuff>\n<some more extra stuff>\n<more stuff>");
- auto& doc = te.GetDocument();
- REQUIRE(doc.size() == 3);
- CHECK(doc[0] == "This <some extra stuff>\n");
- CHECK(doc[1] == "<some more extra stuff>\n");
- CHECK(doc[2] == "<more stuff>is a test line\n");
- }
- SUBCASE("Line break at the end") {
- te.InsertText({ 0, 3 }, "<some extra stuff>\n<some more extra stuff>\n<more stuff>\n");
- auto& doc = te.GetDocument();
- REQUIRE(doc.size() == 3);
- CHECK(doc[0] == "This <some extra stuff>\n");
- CHECK(doc[1] == "<some more extra stuff>\n");
- CHECK(doc[1] == "<more stuff>\n");
- CHECK(doc[2] == "is a test line\n");
- }
- }
- SUBCASE("Removing ranges") {
- te.AppendLine("This is a test line");
- te.RemoveText(TextSelection{
- .begin = { 0, 2 },
- .end = { 0, 10 },
- });
- CHECK(te.GetDocument()[0] == "Thtest line\n");
- }
- SUBCASE("Automatic \\n insertion") {
- te.AppendLine("This is a test line");
- te.AppendLine("This is a test line\n");
- for (auto& line : te.GetDocument()) {
- CHECK(line == "This is a test line\n");
- }
- }
- SUBCASE("Caret horizontal movement") {
- te.AppendLine("This is a test line");
- te.AppendLine("This is another test line");
- SUBCASE("Simple moving between characters") {
- te.SetCaretLine(0);
- te.SetCaretColumn(3);
- AcceptEvent(GLFW_KEY_RIGHT);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 4);
- AcceptEvent(GLFW_KEY_LEFT);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 3);
- }
- SUBCASE("Caret automatic line jumping (right to next line)") {
- te.SetCaretLine(0);
- te.SetCaretColumn(19);
- AcceptEvent(GLFW_KEY_RIGHT);
- CHECK(te.GetCaret().line == 1);
- CHECK(te.GetCaret().column == 0);
- }
- SUBCASE("Caret automatic line jumping (left to previous line)") {
- te.SetCaretLine(1);
- te.SetCaretColumn(0);
- AcceptEvent(GLFW_KEY_LEFT);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 19);
- }
- SUBCASE("Move left at beginning of document does nothing") {
- te.SetCaretLine(0);
- te.SetCaretColumn(0);
- AcceptEvent(GLFW_KEY_LEFT);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 0);
- }
- SUBCASE("Move right at end of document does nothing") {
- te.SetCaretLine(1);
- te.SetCaretColumn(25);
- AcceptEvent(GLFW_KEY_RIGHT);
- CHECK(te.GetCaret().line == 1);
- CHECK(te.GetCaret().column == 25);
- }
- }
- SUBCASE("Caret vertical movement") {
- te.AppendLine("This is a test line");
- te.AppendLine("This is another test line");
- SUBCASE("Simple moving between lines") {
- te.SetCaretLine(0);
- te.SetCaretColumn(2);
- AcceptEvent(GLFW_KEY_DOWN);
- CHECK(te.GetCaret().line == 1);
- CHECK(te.GetCaret().column == 2);
- }
- SUBCASE("Caret column behavior: line clamping and desired column") {
- // Automatic length clamping behavior
- te.SetCaretLine(1);
- te.SetCaretColumn(22); // "This is another test l|i]ne"
- AcceptEvent(GLFW_KEY_UP);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 19);
- // "Desired column" behavior
- AcceptEvent(GLFW_KEY_DOWN);
- CHECK(te.GetCaret().line == 1);
- CHECK(te.GetCaret().column == 22);
- }
- SUBCASE("Move down at end of document does nothing") {
- te.SetCaretLine(1);
- te.SetCaretColumn(0);
- AcceptEvent(GLFW_KEY_DOWN);
- CHECK(te.GetCaret().line == 1);
- CHECK(te.GetCaret().column == 0);
- }
- SUBCASE("Move up at beginning of document does nothing") {
- te.SetCaretLine(0);
- te.SetCaretColumn(0);
- AcceptEvent(GLFW_KEY_UP);
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 0);
- }
- }
- SUBCASE("scrollToFront()") {
- te.AppendLine("This is a test line");
- te.SetCaretLine(0);
- te.SetCaretColumn(5);
- te.ScrollToFront();
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 0);
- }
- SUBCASE("scrollToBack()") {
- te.AppendLine("This is a test line");
- te.SetCaretLine(0);
- te.SetCaretColumn(0);
- te.ScrollToBack();
- CHECK(te.GetCaret().line == 0);
- CHECK(te.GetCaret().column == 19);
- }
- SUBCASE("Selection accessor") {
- te.AppendLine("This is a test line");
- te.AppendLine("This is another test line");
- te.SetSelection(TextSelection{
- .begin = CharLocation{ 0, 3 }, // Thi|s]
- .end = CharLocation{ 1, 10 }, // This is an|o]
- });
- SUBCASE("getSelectedText()") {
- CHECK(te.GetSelectedText() == "s is a test line\nThis is an");
- }
- SUBCASE("getSelectedLines()") {
- auto lines = te.GetSelectedLines();
- REQUIRE(lines.size() == 2);
- CHECK(lines[0] == "s is a test line\n");
- CHECK(lines[1] == "This is an");
- }
- }
- SUBCASE("Selection: shift+arrow key") {
- te.AppendLine("This is a test line");
- te.SetCaretLine(0);
- te.SetCaretColumn(3);
- // Presses shift+right 3 times
- AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
- AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
- AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
- SUBCASE("") {
- CHECK(te.HasSelection() == true);
- CHECK(te.GetSelectedText() == "s i");
- }
- SUBCASE("Moving caret removes selection") {
- AcceptEvent(GLFW_KEY_RIGHT, 0);
- CHECK(te.HasSelection() == false);
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement