Advertisement
hnOsmium0001

UI/TextEdit WIP v1

Jan 31st, 2021
1,017
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 33.25 KB | None | 0 0
  1. /// hpp
  2.  
  3. #pragma once
  4.  
  5. #include "Graphics/Color.hpp"
  6. #include "UI/Widget.hpp"
  7. #include "UI/fwd.hpp"
  8. #include "Utils/Sigslot.hpp"
  9. #include "Utils/UndoRedo.hpp"
  10.  
  11. #include <GL/gl3w.h>
  12. #include <memory>
  13. #include <span>
  14. #include <string>
  15. #include <string_view>
  16. #include <vector>
  17.  
  18. struct CharLocation {
  19.     int line;
  20.     int column;
  21.  
  22.     std::string Format() const;
  23.  
  24.     CharLocation Next(int chars = 1) const;
  25.     CharLocation Prev(int chars = 1) const;
  26.     CharLocation NextLine(int lines = 1) const;
  27.     CharLocation PrevLine(int lines = 1) const;
  28.     CharLocation Downwards(int lines = 1) const;
  29.     CharLocation Upwards(int lines = 1) const;
  30.  
  31.     auto operator<=>(const CharLocation&) const = default;
  32. };
  33.  
  34. /// [begin, end) for chars (column)
  35. /// [begin, end] for lines
  36. /// Example: (10,0) (10,5) includes a 5 char sequence
  37. /// Example: (10,0) (11,0) with line 10 being 5 chars long, includes a 6 char sequence
  38. /// with the last one being the linebreak character
  39. struct TextSelection {
  40.     CharLocation begin;
  41.     CharLocation end;
  42.  
  43.     int GetHorizontalLength() const;
  44.     int GetVerticalLength() const;
  45.  
  46.     std::string Format() const;
  47.     std::string FormatFull() const;
  48. };
  49.  
  50. /// A text editor that only concerns about characters, a single caret, no syntax highlighting, and no folding.
  51. class TextEdit : public Widget {
  52. private:
  53.     /// A list of actions belonging to this TextEdit. This will be used regardless of whether a root
  54.     /// (and its UndoStack) is attached.
  55.     std::vector<std::shared_ptr<IAction>> mActions;
  56.  
  57.     struct Caret {
  58.         /// Index of the line that the caret is on.
  59.         int line = 0;
  60.         /// The "desired" column that the caret wants to be. Clamped with current line length
  61.         /// to get the actual column.
  62.         /// Unit: glyphs
  63.         int desiredColumn = 0;
  64.         /// Unit: glyphs
  65.         int column = 0;
  66.         /// Unit: UTF-8 code units (bytes)
  67.         int byteOffset = 0;
  68.     } mCaret;
  69.  
  70.     /// "Selection Anchor"
  71.     CharLocation mSA{ -1, -1 };
  72.  
  73.     /// Using a shared_ptr allows this to interface with other text storages (such as code blocks)
  74.     /// without maintining 2 copies of the data and the consistency between them.
  75.     std::shared_ptr<std::vector<std::string>> mDocument;
  76.  
  77.     Font* mFont;
  78.     RgbaColor mTextColor;
  79.  
  80.     /// The index of the first drawn line.
  81.     int mLineOffset;
  82.     /// Cache for how many glyphs are currently present in this document. Used for buffer allocation.
  83.     int mGlyphCount;
  84.  
  85.     GLuint mGlyphVertexArray;
  86.     GLuint mGlyphVBO;
  87.     GLuint mGlyphIBO;
  88.     GLsizei mIndices;
  89.     GLuint mCaretVertexArray;
  90.     GLuint mCaretVBO;
  91.  
  92.     bool mNeedsRedraw = false;
  93.  
  94. public:
  95.     TextEdit();
  96.     TextEdit(const std::shared_ptr<std::vector<std::string>>& doc);
  97.     virtual ~TextEdit();
  98.  
  99.     virtual void PostDrawCall() final override;
  100.     void RebuildBuffers();
  101.  
  102.     virtual void MousePressed(MouseEvent& event) override;
  103.     virtual void KeyPressed(KeyEvent& event) override;
  104.     virtual void CharacterInput(CharacterInputEvent& event) override;
  105.  
  106.     Font& GetFont() const;
  107.     void SetFont(Font& font);
  108.  
  109.     RgbaColor GetTextColor() const;
  110.     void SetTextColor(RgbaColor color);
  111.  
  112.     const std::vector<std::string>& GetDocument() const;
  113.     void SetDocument(const std::shared_ptr<std::vector<std::string>>& doc);
  114.     void SetDocument(std::vector<std::string> doc);
  115.  
  116.     CharLocation GetCaret() const;
  117.     void SetCaretLine(int line);
  118.     void SetCaretColumn(int column);
  119.     void ScrollToFront();
  120.     void ScrollToBack();
  121.  
  122.     /// \see InsertText(CharLocation, int, std::string_view)
  123.     bool InsertText(CharLocation loc, std::string_view text);
  124.     /// Insert the given text at the given location, and use `byteOffset` to split the line.
  125.     /// All line breaks ('\n') will be processed and generate corresponding separate lines.
  126.     ///
  127.     /// This method is unsafe: no bound or UTF-8 alignment checks are performed. Use with caution.
  128.     /// (And therefore a success flag is not provided).
  129.     void InsertText(CharLocation loc, int byteOffset, std::string_view text);
  130.     /// Strictly inserts text after the caret, ignoring the selection. Use replaceSelection() if you
  131.     /// want to emulate typing the given text.
  132.     /// This method is gaurenteed to not fail, so a success flag is not provided.
  133.     void InsertTextAtCaret(std::string_view text);
  134.     /// Insert the given `text` into the document, so that the text forms a line with index `line`.
  135.     /// Extra empty lines are inserted if `lien` is outside of the current document range.
  136.     void InsertLine(int line, std::string text);
  137.     void AppendLine(std::string text);
  138.     /// Insert a line break at the given location, and everything after the location on the line will
  139.     /// be moved to a new line.
  140.     bool InsertLineBreak(CharLocation loc);
  141.     bool RemoveText(TextSelection range);
  142.     bool RemoveLine(int line);
  143.     /// Move the range of lines [begin, end) up or down by `distance`. The direction of movement is
  144.     /// determined by the sign of `distance`, where positive indicates down and negative indicates up.
  145.     bool MoveLines(int begin, int end, int distance);
  146.     /// Move the selection by the `distance`. When there is no selection, caret's current line
  147.     /// is considered the "selection".
  148.     ///
  149.     /// \see moveSelection(int, int, int)
  150.     bool MoveSelection(int distance);
  151.  
  152.     bool HasSelection() const;
  153.     TextSelection GetSelection() const;
  154.     void SetSelection(TextSelection selection, bool caretAtEnd = false);
  155.     void ClearSelection();
  156.     std::string GetSelectedText() const;
  157.     std::vector<std::string> GetSelectedLines();
  158.  
  159.     /// Replace the selection if present, otherwise delegate to insertTextAtCaret().
  160.     bool ReplaceSelectionOrInsert(std::string_view text);
  161.     bool ReplaceSelection(std::string_view text);
  162.     bool RemoveSelection();
  163.  
  164.     void CutToClipboard();
  165.     void CopyToClipboard();
  166.     void PasteFromClipboard();
  167.     void DeleteLine();
  168. };
  169.  
  170. /// cpp
  171.  
  172. #define NOMINMAX
  173. #include "TextEdit.hpp"
  174.  
  175. #include "Graphics/Font.hpp"
  176. #include "Graphics/Painter.hpp"
  177. #include "Graphics/Vertex.hpp"
  178. #include "UI/Input.hpp"
  179. #include "UI/RootWidget.hpp"
  180. #include "Utils/String.hpp"
  181.  
  182. #include <GL/gl3w.h>
  183. #include <GLFW/glfw3.h>
  184. #include <doctest/doctest.h>
  185. #include <algorithm>
  186. #include <memory>
  187. #include <utility>
  188.  
  189. #ifndef DOCTEST_CONFIG_DISABLE
  190. #   include "UI/RootWidget.hpp"
  191. #endif
  192.  
  193. std::string CharLocation::Format() const {
  194.     return std::to_string(line + 1) + ":" + std::to_string(column + 1);
  195. }
  196.  
  197. CharLocation CharLocation::Next(int chars) const {
  198.     return { line, column + chars };
  199. }
  200.  
  201. CharLocation CharLocation::Prev(int chars) const {
  202.     return { line, column - chars };
  203. }
  204.  
  205. CharLocation CharLocation::NextLine(int lines) const {
  206.     return { line + lines, 0 };
  207. }
  208.  
  209. CharLocation CharLocation::PrevLine(int lines) const {
  210.     return { line - lines, 0 };
  211. }
  212.  
  213. CharLocation CharLocation::Downwards(int lines) const {
  214.     return { line + lines, column };
  215. }
  216.  
  217. CharLocation CharLocation::Upwards(int lines) const {
  218.     return { line - lines, column };
  219. }
  220.  
  221. TEST_CASE("CharLocation line difference") {
  222.     CharLocation l1{ 0, 0 };
  223.     CharLocation l2{ 3, 0 };
  224.     CHECK(l1 < l2);
  225. }
  226.  
  227. TEST_CASE("CharLocation column difference") {
  228.     CharLocation l1{ 2, 4 };
  229.     CharLocation l2{ 2, 8 };
  230.     CHECK(l1 < l2);
  231. }
  232.  
  233. TEST_CASE("CharLocation line & column difference") {
  234.     CharLocation l1{ 0, 12 };
  235.     CharLocation l2{ 2, 7 };
  236.     CHECK(l1 < l2);
  237. }
  238.  
  239. int TextSelection::GetHorizontalLength() const {
  240.     return std::abs(end.column - begin.column);
  241. }
  242.  
  243. int TextSelection::GetVerticalLength() const {
  244.     return end.line - begin.line + 1;
  245. }
  246.  
  247. std::string TextSelection::Format() const {
  248.     return begin.Format();
  249. }
  250.  
  251. std::string TextSelection::FormatFull() const {
  252.     return "[" + begin.Format() + ", " + end.Format() + "]";
  253. }
  254.  
  255. namespace {
  256. class TextManipulationAction : public IAction {
  257. protected:
  258.     TextEdit* mTextEdit;
  259.     std::string mText;
  260.     TextSelection mRange;
  261.  
  262. public:
  263.     TextManipulationAction(TextEdit& widget, std::string text, const TextSelection& range)
  264.         : mTextEdit{ &widget }
  265.         , mText{ std::move(text) }
  266.         , mRange{ range } {
  267.     }
  268. };
  269.  
  270. class InsertAction : public TextManipulationAction {
  271. public:
  272.     virtual void DoAction() override {
  273.         mTextEdit->InsertText(mRange.begin, mText);
  274.     }
  275.  
  276.     virtual void UndoAction() override {
  277.         mTextEdit->RemoveText(mRange);
  278.     }
  279. };
  280.  
  281. class DeleteAction : public TextManipulationAction {
  282. public:
  283.     virtual void DoAction() override {
  284.         mTextEdit->RemoveText(mRange);
  285.     }
  286.  
  287.     virtual void UndoAction() override {
  288.         mTextEdit->InsertText(mRange.begin, mText);
  289.     }
  290. };
  291. } // namespace
  292.  
  293. TextEdit::TextEdit()
  294.     : TextEdit(std::make_shared<std::vector<std::string>>()) {
  295. }
  296.  
  297. TextEdit::TextEdit(const std::shared_ptr<std::vector<std::string>>& doc)
  298.     : mDocument{ doc }
  299.     , mFont{ Font::sans } {
  300. #ifdef DOCTEST_CONFIG_DISABLE
  301.     glGenVertexArrays(1, &mGlyphVertexArray);
  302.     glGenBuffers(1, &mGlyphVBO);
  303.     glGenBuffers(1, &mGlyphIBO);
  304.  
  305.     SetHasSpecialDrawRoutine(true);
  306. #else
  307.     // Do nothing
  308. #endif
  309. }
  310.  
  311. TextEdit::~TextEdit() {
  312. #ifdef DOCTEST_CONFIG_DISABLE
  313.     glDeleteBuffers(1, &mGlyphVertexArray);
  314.     glDeleteBuffers(1, &mGlyphVBO);
  315.     glDeleteBuffers(1, &mGlyphIBO);
  316. #else
  317.     // Do nothing
  318. #endif
  319. }
  320.  
  321. void TextEdit::PostDrawCall() {
  322. #ifdef DOCTEST_CONFIG_DISABLE
  323.     if (mNeedsRedraw) {
  324.         RebuildBuffers();
  325.     }
  326.  
  327.     glBindVertexArray(mGlyphVertexArray);
  328.     glDrawElements(GL_TRIANGLES, mIndices, GL_UNSIGNED_SHORT, (void*)0);
  329. #else
  330.     // Do nothing
  331. #endif
  332. }
  333.  
  334. void TextEdit::RebuildBuffers() {
  335. #ifdef DOCTEST_CONFIG_DISABLE
  336.     auto textVertices = std::make_unique<TexturedVertex[]>(mGlyphCount * 4);
  337.     auto textIndices = std::make_unique<uint16_t[]>(mGlyphCount * 6);
  338.  
  339.     Font::DrawTargetPointer target{
  340.         .vertices = textVertices.get(),
  341.         .indices = textIndices.get(),
  342.         .initialVertexIdx = 0,
  343.         .pos = glm::vec3{ GetGlobalGeometry().TopLeft(), GetDepth() },
  344.         .color = mTextColor,
  345.     };
  346.     for (auto& line : *mDocument) {
  347.         mFont->DrawTo(line, target);
  348.         target.vertices += 4;
  349.         target.indices += 6;
  350.         target.initialVertexIdx += 4;
  351.         target.pos.y += mFont->GetFontHeight();
  352.     }
  353.  
  354.     glBindBuffer(GL_ARRAY_BUFFER, mGlyphVBO);
  355.     glBufferData(GL_ARRAY_BUFFER, mGlyphCount * 4 * sizeof(TexturedVertex), textVertices.get(), GL_STATIC_DRAW);
  356.     glBindBuffer(GL_ARRAY_BUFFER, mGlyphIBO);
  357.     glBufferData(GL_ARRAY_BUFFER, mGlyphCount * 6 * sizeof(uint16_t), textIndices.get(), GL_STATIC_DRAW);
  358.  
  359.     ColoredVertex caretVertices[6];
  360.     // TODO calc caret pos
  361.     // TODO draw caret
  362.  
  363.     glBindBuffer(GL_ARRAY_BUFFER, mCaretVBO);
  364.     glBufferData(GL_ARRAY_BUFFER, 2 * 3 * sizeof(ColoredVertex), caretVertices, GL_DYNAMIC_DRAW);
  365.  
  366.     mNeedsRedraw = false;
  367. #else
  368.     // 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)
  369. #endif
  370. }
  371.  
  372. void TextEdit::MousePressed(MouseEvent& event) {
  373.     if (event.IsConsumed()) {
  374.         return;
  375.     }
  376.  
  377.     // TODO
  378. }
  379.  
  380. void TextEdit::KeyPressed(KeyEvent& event) {
  381.     if (event.IsConsumed()) {
  382.         return;
  383.     }
  384.  
  385.     auto UpdateSelectionAnchor = [&]() {
  386.         if (event.HasMod(GLFW_MOD_SHIFT)) {
  387.             if (!HasSelection()) {
  388.                 mSA.line = mCaret.line;
  389.                 mSA.column = mCaret.column;
  390.             }
  391.         } else {
  392.             // Clear selection regardless of whether the caret moved or not
  393.             ClearSelection();
  394.         }
  395.     };
  396.  
  397.     switch (event.key) {
  398.         case GLFW_KEY_LEFT:
  399.         case GLFW_KEY_RIGHT: {
  400.             UpdateSelectionAnchor();
  401.  
  402.             std::string_view line{ (*mDocument)[mCaret.line] };
  403.             if (mCaret.byteOffset == 0 && event.key == GLFW_KEY_LEFT) {
  404.                 // Start of the document, can't move left anymore
  405.                 if (mCaret.line == 0) {
  406.                     ClearSelection(); // Clear selection in case we reach the GLFW_MOD_SHIFT && hasSelection() branch
  407.                     break;
  408.                 }
  409.  
  410.                 mCaret.line--;
  411.                 line = (*mDocument)[mCaret.line];
  412.  
  413.                 // Last character in previous line, i.e. the '\n'
  414.                 auto [index, byteOffset] = StringLastCodepoint(line);
  415.                 mCaret.column = index;
  416.                 mCaret.desiredColumn = index;
  417.                 mCaret.byteOffset = byteOffset;
  418.             } else if (mCaret.byteOffset == line.length() - 1 && event.key == GLFW_KEY_RIGHT) {
  419.                 // End of the document, can't move right anymore
  420.                 if (mCaret.line == mDocument->size() - 1) {
  421.                     ClearSelection();
  422.                     break;
  423.                 }
  424.  
  425.                 mCaret.line++;
  426.                 line = (*mDocument)[mCaret.line];
  427.  
  428.                 mCaret.column = 0;
  429.                 mCaret.desiredColumn = 0;
  430.                 mCaret.byteOffset = 0;
  431.             } else {
  432.                 int col = mCaret.column;
  433.                 Utf8Iterator it{ line.begin() + mCaret.byteOffset };
  434.                 if (event.key == GLFW_KEY_LEFT) {
  435.                     col -= 1;
  436.                     it--;
  437.                 } else {
  438.                     col += 1;
  439.                     it++;
  440.                 }
  441.  
  442.                 mCaret.column = col;
  443.                 mCaret.desiredColumn = col;
  444.                 mCaret.byteOffset = std::distance(line.begin(), it.AsInternal());
  445.             }
  446.  
  447.             // Normal exit condition: successfull
  448.             event.Consume();
  449.         } break;
  450.  
  451.         case GLFW_KEY_DOWN: {
  452.             constexpr Modifiers kMask = GLFW_MOD_CONTROL | GLFW_MOD_SHIFT;
  453.             if ((event.modifiers & kMask) == kMask) {
  454.                 MoveSelection(1);
  455.                 break;
  456.             }
  457.  
  458.             UpdateSelectionAnchor();
  459.             if (mCaret.line == mDocument->size() - 1) {
  460.                 ClearSelection();
  461.                 break;
  462.             }
  463.  
  464.             SetCaretLine(mCaret.line + 1);
  465.             event.Consume();
  466.         } break;
  467.  
  468.         case GLFW_KEY_UP: {
  469.             constexpr Modifiers kMask = GLFW_MOD_CONTROL | GLFW_MOD_SHIFT;
  470.             if ((event.modifiers & kMask) == kMask) {
  471.                 MoveSelection(-1);
  472.                 break;
  473.             }
  474.  
  475.             UpdateSelectionAnchor();
  476.             if (mCaret.line == 0) {
  477.                 ClearSelection();
  478.                 break;
  479.             }
  480.  
  481.             SetCaretLine(mCaret.line - 1);
  482.             event.Consume();
  483.         } break;
  484.  
  485.         case GLFW_KEY_HOME: {
  486.             UpdateSelectionAnchor();
  487.             ScrollToFront();
  488.  
  489.             event.Consume();
  490.             break;
  491.         }
  492.  
  493.         case GLFW_KEY_END: {
  494.             UpdateSelectionAnchor();
  495.             ScrollToBack();
  496.  
  497.             event.Consume();
  498.             break;
  499.         }
  500.  
  501.         case GLFW_KEY_ENTER: {
  502.             auto loc = GetCaret();
  503.             if (HasSelection()) {
  504.                 RemoveSelection();
  505.             }
  506.             InsertLineBreak(loc);
  507.  
  508.             event.Consume();
  509.             break;
  510.         }
  511.  
  512.         case GLFW_KEY_BACKSPACE: {
  513.             if (HasSelection()) {
  514.                 RemoveText(GetSelection());
  515.             } else if (mCaret.column == 0) {
  516.                 if (mCaret.line > 0) {
  517.                     auto& prevLine = (*mDocument)[mCaret.line - 1];
  518.                     auto& line = (*mDocument)[mCaret.line];
  519.  
  520.                     prevLine.pop_back(); // Remove '\n'
  521.                     prevLine += line;
  522.  
  523.                     RemoveLine(mCaret.line);
  524.                     mNeedsRedraw = true;
  525.                 }
  526.             } else {
  527.                 auto& line = (*mDocument)[mCaret.line];
  528.  
  529.                 std::string_view sv{ line };
  530.                 Utf8Iterator it{ sv.begin() + mCaret.byteOffset };
  531.                 it--;
  532.                 auto leftOffset = std::distance(sv.begin(), it.AsInternal());
  533.  
  534.                 line = line.substr(0, leftOffset); // Everything before 1 before caret
  535.                 line += line.substr(mCaret.byteOffset); // Everything after (inclusive) caret
  536.                 mNeedsRedraw = true;
  537.             }
  538.  
  539.             event.Consume();
  540.             break;
  541.         }
  542.  
  543.         case GLFW_KEY_DELETE: {
  544.             if (HasSelection()) {
  545.                 RemoveText(GetSelection());
  546.             } else if (mCaret.byteOffset == (*mDocument)[mCaret.line].length()) {
  547.                 if (mCaret.line < mDocument->size() - 1) {
  548.                     auto& line = (*mDocument)[mCaret.line];
  549.                     auto& nextLine = (*mDocument)[mCaret.line + 1];
  550.  
  551.                     line.pop_back(); // Remove '\n'
  552.                     line += nextLine;
  553.  
  554.                     RemoveLine(mCaret.line + 1);
  555.                     mNeedsRedraw = true;
  556.                 }
  557.             } else {
  558.                 auto& line = (*mDocument)[mCaret.line];
  559.  
  560.                 std::string_view sv{ line };
  561.                 Utf8Iterator it{ sv.begin() + mCaret.byteOffset };
  562.                 it++;
  563.                 auto rightOffset = std::distance(sv.begin(), it.AsInternal());
  564.  
  565.                 line = line.substr(0, mCaret.byteOffset); // Everything before caret
  566.                 line += line.substr(rightOffset); // Everything after (exclusive) caret
  567.                 mNeedsRedraw = true;
  568.             }
  569.  
  570.             event.Consume();
  571.             break;
  572.         }
  573.  
  574.         case GLFW_KEY_X: {
  575.             if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
  576.                 event.Consume();
  577.                 CutToClipboard();
  578.             }
  579.         } break;
  580.  
  581.         case GLFW_KEY_C: {
  582.             if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
  583.                 event.Consume();
  584.                 CopyToClipboard();
  585.             }
  586.         } break;
  587.  
  588.         case GLFW_KEY_V: {
  589.             if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
  590.                 event.Consume();
  591.                 PasteFromClipboard();
  592.             }
  593.         } break;
  594.  
  595.         case GLFW_KEY_Y: {
  596.             if (event.HasModExclusive(GLFW_MOD_CONTROL)) {
  597.                 event.Consume();
  598.                 DeleteLine();
  599.             }
  600.         } break;
  601.     }
  602. }
  603.  
  604. void TextEdit::CharacterInput(CharacterInputEvent& event) {
  605.     std::u32string str;
  606.     str += event.character;
  607.  
  608.     InsertTextAtCaret(ConvertUtf32To8(str));
  609. }
  610.  
  611. Font& TextEdit::GetFont() const {
  612.     return *mFont;
  613. }
  614.  
  615. void TextEdit::SetFont(Font& font) {
  616.     mFont = &font;
  617.     mNeedsRedraw = true;
  618. }
  619.  
  620. RgbaColor TextEdit::GetTextColor() const {
  621.     return mTextColor;
  622. }
  623.  
  624. void TextEdit::SetTextColor(RgbaColor color) {
  625.     mTextColor = color;
  626.     mNeedsRedraw = true;
  627. }
  628.  
  629. const std::vector<std::string>& TextEdit::GetDocument() const {
  630.     return *mDocument;
  631. }
  632.  
  633. void TextEdit::SetDocument(const std::shared_ptr<std::vector<std::string>>& doc) {
  634.     mDocument = doc;
  635.     mActions.clear();
  636.     mNeedsRedraw = true;
  637. }
  638.  
  639. void TextEdit::SetDocument(std::vector<std::string> doc) {
  640.     mDocument = std::make_shared<std::vector<std::string>>(std::move(doc));
  641.     mActions.clear();
  642.     mNeedsRedraw = true;
  643. }
  644.  
  645. CharLocation TextEdit::GetCaret() const {
  646.     return { mCaret.line, mCaret.column };
  647. }
  648.  
  649. void TextEdit::SetCaretLine(int lineNumber) {
  650.     mCaret.line = std::clamp(lineNumber, 0, (int)mDocument->size() - 1);
  651.  
  652.     auto& line = (*mDocument)[mCaret.line];
  653.     auto [clamped, byteOffset] = StringCodepoint(line, mCaret.desiredColumn);
  654.     mCaret.column = clamped;
  655.     mCaret.byteOffset = byteOffset;
  656. }
  657.  
  658. void TextEdit::SetCaretColumn(int column) {
  659.     mCaret.desiredColumn = column;
  660.  
  661.     auto& line = (*mDocument)[mCaret.line];
  662.     auto [clamped, byteOffset] = StringCodepoint(line, column);
  663.     mCaret.column = clamped;
  664.     mCaret.byteOffset = byteOffset;
  665. }
  666.  
  667. void TextEdit::ScrollToFront() {
  668.     mCaret.column = 0;
  669.     mCaret.desiredColumn = 0;
  670.     mCaret.byteOffset = 0;
  671. }
  672.  
  673. void TextEdit::ScrollToBack() {
  674.     auto& line = (*mDocument)[mCaret.line];
  675.     auto [index, byteOfset] = StringLastCodepoint(line);
  676.     mCaret.column = index;
  677.     mCaret.desiredColumn = index;
  678.     mCaret.byteOffset = byteOfset;
  679. }
  680.  
  681. bool TextEdit::InsertText(CharLocation loc, std::string_view text) {
  682.     if (loc.line < 0 || loc.line >= mDocument->size()) {
  683.         return false;
  684.     }
  685.  
  686.     auto& line = (*mDocument)[loc.line];
  687.     auto [clamped, byteOffset] = StringCodepoint(line, loc.column);
  688.     loc.column = clamped;
  689.  
  690.     InsertText(loc, byteOffset, text);
  691.     return true;
  692. }
  693.  
  694. void TextEdit::InsertText(CharLocation loc, int byteOffset, std::string_view text) {
  695.     std::vector<std::string> textLines;
  696.     std::string buf;
  697.     for (char c : text) {
  698.         if (c == '\n') {
  699.             textLines.push_back(std::move(buf));
  700.             buf.clear();
  701.         } else {
  702.             buf += c;
  703.         }
  704.     }
  705.     if (!buf.empty()) {
  706.         textLines.push_back(std::move(buf));
  707.     }
  708.  
  709.     auto& firstLine = (*mDocument)[loc.line];
  710.     auto front = firstLine.substr(0, byteOffset);
  711.     auto back = firstLine.substr(byteOffset);
  712.     if (textLines[0].back() == '\n') {
  713.         firstLine = front + textLines[0];
  714.  
  715.         if (textLines.size() >= 2) {
  716.             mDocument->insert(
  717.                 mDocument->begin() + loc.line,
  718.                 std::make_move_iterator(textLines.begin() + 1),
  719.                 std::make_move_iterator(textLines.end()));
  720.  
  721.             if (textLines.back().back() == '\n') {
  722.                 // Last line has a line break, `back` should be inserted as a separate line
  723.                 mDocument->insert(
  724.                     mDocument->begin() + loc.line + textLines.size(),
  725.                     back);
  726.             } else {
  727.                 // `back` should be inserted after the last line
  728.                 (*mDocument)[loc.line + textLines.size()] += back;
  729.             }
  730.         }
  731.     } else {
  732.         // `buf` isn't empty: there was no \n, it's going to be stuffed between `front` and `back`
  733.         firstLine = front + textLines[0] + back;
  734.     }
  735.  
  736.     // TODO handle caret positioning
  737. }
  738.  
  739. void TextEdit::InsertTextAtCaret(std::string_view text) {
  740.     InsertText(GetCaret(), text);
  741. }
  742.  
  743. void TextEdit::InsertLine(int line, std::string text) {
  744.     if (text.back() != '\n') {
  745.         text += '\n';
  746.     }
  747.  
  748.     if (line < 0) {
  749.         std::vector<std::string> cand(-line + 1);
  750.         cand[0] = std::move(text);
  751.  
  752.         mDocument->insert(mDocument->begin(), cand.begin(), cand.end());
  753.     } else {
  754.         mDocument->insert(mDocument->begin() + line, std::move(text));
  755.     }
  756.  
  757.     if (mCaret.line > line) {
  758.         mCaret.line--;
  759.     }
  760.     if (HasSelection() && mSA.line > line) {
  761.         mSA.line--;
  762.     }
  763.  
  764.     mNeedsRedraw = true;
  765. }
  766.  
  767. void TextEdit::AppendLine(std::string text) {
  768.     if (text.back() != '\n') {
  769.         text.push_back('\n');
  770.     }
  771.  
  772.     mDocument->push_back(std::move(text));
  773.     mNeedsRedraw = true;
  774. }
  775.  
  776. bool TextEdit::InsertLineBreak(CharLocation loc) {
  777.     if (loc.line < 0 || loc.line >= mDocument->size()) return false;
  778.  
  779.     auto& line = (*mDocument)[loc.line];
  780.     auto [_, byteOffset] = StringCodepoint(line, loc.column);
  781.  
  782.     auto front = line.substr(0, byteOffset);
  783.     auto back = line.substr(byteOffset);
  784.     line = front + "\n";
  785.     InsertLine(loc.line + 1, back);
  786.  
  787.     return true;
  788. }
  789.  
  790. bool TextEdit::RemoveText(TextSelection range) {
  791.     if (range.begin.line < 0 || range.begin.line >= mDocument->size()) return false;
  792.     if (range.end.line < 0 || range.end.line >= mDocument->size()) return false;
  793.     if (range.begin.line > range.end.line) return false;
  794.  
  795.     if (range.begin.line == range.end.line) {
  796.         auto& line = (*mDocument)[range.begin.line];
  797.  
  798.         auto begin = StringCodepoint(line, range.begin.column).byteOffset;
  799.         auto end = StringCodepoint(line, range.end.column).byteOffset;
  800.  
  801.         auto front = line.substr(0, begin);
  802.         auto back = line.substr(end);
  803.         line = front + back;
  804.     } else {
  805.         auto& first = (*mDocument)[range.begin.line];
  806.         first.resize(StringCodepoint(first, range.begin.column).byteOffset);
  807.  
  808.         // More than 2 lines total, i.e. have middle parts
  809.         if (range.end.line - range.begin.line + 1 >= 2) {
  810.             auto begin = mDocument->begin() + range.begin.line + 1;
  811.             auto end = mDocument->end() + range.end.line;
  812.             mDocument->erase(begin, end);
  813.         }
  814.  
  815.         auto& last = (*mDocument)[range.end.line];
  816.         last = last.substr(StringCodepoint(last, range.end.column).byteOffset);
  817.     }
  818.     return true;
  819. }
  820.  
  821. bool TextEdit::RemoveLine(int line) {
  822.     if (line < 0 || line >= mDocument->size()) {
  823.         return false;
  824.     }
  825.  
  826.     mDocument->erase(mDocument->begin() + line);
  827.  
  828.     // Don't need to recalculate column: we are still on the same line (contents), it's just line number changed
  829.     if (mCaret.line > line) {
  830.         mCaret.line--;
  831.     } else if (mCaret.line == line) {
  832.         if (line == mDocument->size() - 1) {
  833.             mDocument->push_back("\n");
  834.             mCaret.column = 0;
  835.             mCaret.byteOffset = 0;
  836.         } else {
  837.             SetCaretLine(mCaret.line);
  838.         }
  839.     }
  840.     if (HasSelection()) {
  841.         if (mSA.line > line) {
  842.             mSA.line--;
  843.         } else if (mSA.line == line) {
  844.             auto& line = (*mDocument)[mSA.line];
  845.             auto [column, _] = StringCodepoint(line, mSA.column);
  846.             mSA.column = column;
  847.         }
  848.     }
  849.  
  850.     mNeedsRedraw = true;
  851.     return true;
  852. }
  853.  
  854. bool TextEdit::MoveLines(int begin, int end, int distance) {
  855.     if (distance > 0) {
  856.         // Affects range [begin, end + |distance|)
  857.         if (begin < 0) return false;
  858.         if (end + distance >= mDocument->size()) return false;
  859.  
  860.         // Make the non-"selection" the first in range
  861.         std::rotate(
  862.             mDocument->begin() + begin,
  863.             mDocument->begin() + end,
  864.             mDocument->begin() + end + distance);
  865.     } else {
  866.         distance = -distance; // Make positive
  867.  
  868.         // Affects range [begin - |distance|, end)
  869.         if (begin - distance < 0) return false;
  870.         if (end >= mDocument->size()) return false;
  871.  
  872.         // Make "selection" the first in range
  873.         std::rotate(
  874.             mDocument->begin() + begin - distance,
  875.             mDocument->begin() + begin,
  876.             mDocument->begin() + end);
  877.     }
  878.     return true;
  879. }
  880.  
  881. bool TextEdit::MoveSelection(int distance) {
  882.     if (HasSelection()) {
  883.         auto sel = GetSelection();
  884.         return MoveLines(sel.begin.line, sel.end.line + 1, distance);
  885.     } else {
  886.         return MoveLines(mCaret.line, mCaret.line + 1, distance);
  887.     }
  888. }
  889.  
  890. bool TextEdit::HasSelection() const {
  891.     return mSA.line != -1 && mSA.column != -1;
  892. }
  893.  
  894. TextSelection TextEdit::GetSelection() const {
  895.     return TextSelection{
  896.         .begin = std::min(GetCaret(), mSA),
  897.         .end = std::max(GetCaret(), mSA),
  898.     };
  899. }
  900.  
  901. void TextEdit::SetSelection(TextSelection selection, bool caretAtEnd) {
  902.     selection.begin.line = std::clamp(selection.begin.line, 0, (int)mDocument->size() - 1);
  903.     selection.end.line = std::clamp(selection.end.line, 0, (int)mDocument->size() - 1);
  904.  
  905.     auto assign = [&](CharLocation sa, CharLocation caret) {
  906.         mSA.line = sa.line;
  907.         mSA.column = sa.column;
  908.         mCaret.line = caret.line;
  909.         mCaret.column = caret.column;
  910.         mCaret.desiredColumn = caret.column;
  911.     };
  912.  
  913.     if (caretAtEnd) {
  914.         assign(selection.begin, selection.end);
  915.     } else {
  916.         assign(selection.end, selection.begin);
  917.     }
  918.     mCaret.byteOffset = StringCodepoint((*mDocument)[mCaret.line], mCaret.column).byteOffset;
  919. }
  920.  
  921. void TextEdit::ClearSelection() {
  922.     mSA.line = -1;
  923.     mSA.column = -1;
  924. }
  925.  
  926. std::string TextEdit::GetSelectedText() const {
  927.     auto sel = GetSelection();
  928.     auto& lines = *mDocument;
  929.  
  930.     std::string res;
  931.     if (sel.begin.line == sel.end.line) {
  932.         std::string_view line{ lines[sel.begin.line] };
  933.         res.append(StringRange(line, sel.begin.column, sel.end.column));
  934.     } else {
  935.         res.append(std::string_view(lines[sel.begin.line]).substr(sel.begin.column));
  936.         for (int i = sel.begin.line + 1; i < sel.end.line; ++i) {
  937.             res.append(lines[i]);
  938.         }
  939.         res.append(std::string_view(lines[sel.end.line]).substr(0, sel.end.column));
  940.     }
  941.     return res;
  942. }
  943.  
  944. std::vector<std::string> TextEdit::GetSelectedLines() {
  945.     auto sel = GetSelection();
  946.     auto& lines = *mDocument;
  947.  
  948.     std::vector<std::string> res;
  949.     res.reserve(sel.end.line - sel.begin.line + 1);
  950.  
  951.     if (sel.begin.line == sel.end.line) {
  952.         std::string_view line{ lines[sel.begin.line] };
  953.         std::string range(StringRange(line, sel.begin.column, sel.end.column));
  954.         res.push_back(std::move(range));
  955.     } else {
  956.         res.push_back(lines[sel.begin.line].substr(sel.begin.column));
  957.         for (int i = sel.begin.line + 1; i < sel.end.line; ++i) {
  958.             res.push_back(lines[i]);
  959.         }
  960.         res.push_back(lines[sel.end.line].substr(0, sel.end.column));
  961.     }
  962.     return res;
  963. }
  964.  
  965. bool TextEdit::ReplaceSelectionOrInsert(std::string_view text) {
  966.     if (HasSelection()) {
  967.         return ReplaceSelection(text);
  968.     } else {
  969.         InsertTextAtCaret(text);
  970.         return true;
  971.     }
  972. }
  973.  
  974. bool TextEdit::ReplaceSelection(std::string_view text) {
  975.     if (HasSelection()) {
  976.         auto sel = GetSelection();
  977.         RemoveText(sel);
  978.         InsertText(sel.begin, text);
  979.         return true;
  980.     }
  981.     return false;
  982. }
  983.  
  984. bool TextEdit::RemoveSelection() {
  985.     if (HasSelection()) {
  986.         RemoveText(GetSelection());
  987.         return true;
  988.     }
  989.     return false;
  990. }
  991.  
  992. void TextEdit::CutToClipboard() {
  993.     if (HasSelection()) {
  994.         if (mRoot) {
  995.             auto text = GetSelectedText();
  996.             mRoot->GetIO().SetClipboardText(text.c_str());
  997.         }
  998.         RemoveSelection();
  999.     } else {
  1000.         if (mRoot) {
  1001.             auto& text = (*mDocument)[mCaret.line];
  1002.             mRoot->GetIO().SetClipboardText(text.c_str());
  1003.         }
  1004.         RemoveLine(mCaret.line);
  1005.     }
  1006. }
  1007.  
  1008. void TextEdit::CopyToClipboard() {
  1009.     if (!mRoot) return;
  1010.  
  1011.     if (HasSelection()) {
  1012.         auto text = GetSelectedText();
  1013.         mRoot->GetIO().SetClipboardText(text.c_str());
  1014.     } else {
  1015.         auto text = (*mDocument)[mCaret.line];
  1016.         mRoot->GetIO().SetClipboardText(text.c_str());
  1017.     }
  1018. }
  1019.  
  1020. void TextEdit::PasteFromClipboard() {
  1021.     if (!mRoot) return;
  1022.  
  1023.     auto text = mRoot->GetIO().GetClipboardText();
  1024.     if (HasSelection()) {
  1025.         ReplaceSelection(text);
  1026.     } else {
  1027.         InsertTextAtCaret(text);
  1028.     }
  1029. }
  1030.  
  1031. void TextEdit::DeleteLine() {
  1032.     ClearSelection();
  1033.     RemoveLine(mCaret.line);
  1034. }
  1035.  
  1036. TEST_CASE("TextEdit tests") {
  1037.     InputState io(nullptr);
  1038.     RootWidget root(io);
  1039.  
  1040.     TextEdit te;
  1041.     root.AddWidget(&te);
  1042.  
  1043.     auto AcceptEvent = [&](int key, int modifiers = 0) {
  1044.         KeyEvent event{
  1045.             .key = key,
  1046.             .scancode = -1, // Doesn't matter
  1047.             .modifiers = modifiers,
  1048.         };
  1049.         te.KeyPressed(event);
  1050.     };
  1051.  
  1052.     SUBCASE("Inserting lines") {
  1053.         te.InsertLine(0, "This is a test line");
  1054.         te.InsertLine(0, "This is another test line");
  1055.         CHECK(te.GetDocument()[0] == "This is another test line\n");
  1056.         CHECK(te.GetDocument()[1] == "This is a test line\n");
  1057.     }
  1058.  
  1059.     SUBCASE("Removing lines") {
  1060.         te.AppendLine("This is a test line");
  1061.         te.AppendLine("<junk>");
  1062.         te.AppendLine("This is another test line");
  1063.         CHECK(te.GetDocument()[1] == "<junk>\n");
  1064.         te.RemoveLine(1);
  1065.         CHECK(te.GetDocument().size() == 2);
  1066.         CHECK(te.GetDocument()[1] == "This is another test line\n");
  1067.     }
  1068.  
  1069.     SUBCASE("Inserting ranges") {
  1070.         te.AppendLine("This is a test line");
  1071.         te.AppendLine("This is another test line");
  1072.         te.InsertText(CharLocation{ 0, 5 }, "<some extra stuff>");
  1073.         CHECK(te.GetDocument()[0] == "This <some extra stuff>is a test line\n");
  1074.     }
  1075.  
  1076.     SUBCASE("Inserting multi-line ranges") {
  1077.         te.AppendLine("This is a test line");
  1078.  
  1079.         SUBCASE("No line break at the end") {
  1080.             te.InsertText({ 0, 3 }, "<some extra stuff>\n<some more extra stuff>\n<more stuff>");
  1081.  
  1082.             auto& doc = te.GetDocument();
  1083.             REQUIRE(doc.size() == 3);
  1084.             CHECK(doc[0] == "This <some extra stuff>\n");
  1085.             CHECK(doc[1] == "<some more extra stuff>\n");
  1086.             CHECK(doc[2] == "<more stuff>is a test line\n");
  1087.         }
  1088.  
  1089.         SUBCASE("Line break at the end") {
  1090.             te.InsertText({ 0, 3 }, "<some extra stuff>\n<some more extra stuff>\n<more stuff>\n");
  1091.  
  1092.             auto& doc = te.GetDocument();
  1093.             REQUIRE(doc.size() == 3);
  1094.             CHECK(doc[0] == "This <some extra stuff>\n");
  1095.             CHECK(doc[1] == "<some more extra stuff>\n");
  1096.             CHECK(doc[1] == "<more stuff>\n");
  1097.             CHECK(doc[2] == "is a test line\n");
  1098.         }
  1099.     }
  1100.  
  1101.     SUBCASE("Removing ranges") {
  1102.         te.AppendLine("This is a test line");
  1103.         te.RemoveText(TextSelection{
  1104.             .begin = { 0, 2 },
  1105.             .end = { 0, 10 },
  1106.         });
  1107.         CHECK(te.GetDocument()[0] == "Thtest line\n");
  1108.     }
  1109.  
  1110.     SUBCASE("Automatic \\n insertion") {
  1111.         te.AppendLine("This is a test line");
  1112.         te.AppendLine("This is a test line\n");
  1113.         for (auto& line : te.GetDocument()) {
  1114.             CHECK(line == "This is a test line\n");
  1115.         }
  1116.     }
  1117.  
  1118.     SUBCASE("Caret horizontal movement") {
  1119.         te.AppendLine("This is a test line");
  1120.         te.AppendLine("This is another test line");
  1121.  
  1122.         SUBCASE("Simple moving between characters") {
  1123.             te.SetCaretLine(0);
  1124.             te.SetCaretColumn(3);
  1125.  
  1126.             AcceptEvent(GLFW_KEY_RIGHT);
  1127.             CHECK(te.GetCaret().line == 0);
  1128.             CHECK(te.GetCaret().column == 4);
  1129.  
  1130.             AcceptEvent(GLFW_KEY_LEFT);
  1131.             CHECK(te.GetCaret().line == 0);
  1132.             CHECK(te.GetCaret().column == 3);
  1133.         }
  1134.  
  1135.         SUBCASE("Caret automatic line jumping (right to next line)") {
  1136.             te.SetCaretLine(0);
  1137.             te.SetCaretColumn(19);
  1138.  
  1139.             AcceptEvent(GLFW_KEY_RIGHT);
  1140.             CHECK(te.GetCaret().line == 1);
  1141.             CHECK(te.GetCaret().column == 0);
  1142.         }
  1143.  
  1144.         SUBCASE("Caret automatic line jumping (left to previous line)") {
  1145.             te.SetCaretLine(1);
  1146.             te.SetCaretColumn(0);
  1147.  
  1148.             AcceptEvent(GLFW_KEY_LEFT);
  1149.             CHECK(te.GetCaret().line == 0);
  1150.             CHECK(te.GetCaret().column == 19);
  1151.         }
  1152.  
  1153.         SUBCASE("Move left at beginning of document does nothing") {
  1154.             te.SetCaretLine(0);
  1155.             te.SetCaretColumn(0);
  1156.  
  1157.             AcceptEvent(GLFW_KEY_LEFT);
  1158.             CHECK(te.GetCaret().line == 0);
  1159.             CHECK(te.GetCaret().column == 0);
  1160.         }
  1161.  
  1162.         SUBCASE("Move right at end of document does nothing") {
  1163.             te.SetCaretLine(1);
  1164.             te.SetCaretColumn(25);
  1165.  
  1166.             AcceptEvent(GLFW_KEY_RIGHT);
  1167.             CHECK(te.GetCaret().line == 1);
  1168.             CHECK(te.GetCaret().column == 25);
  1169.         }
  1170.     }
  1171.  
  1172.     SUBCASE("Caret vertical movement") {
  1173.         te.AppendLine("This is a test line");
  1174.         te.AppendLine("This is another test line");
  1175.  
  1176.         SUBCASE("Simple moving between lines") {
  1177.             te.SetCaretLine(0);
  1178.             te.SetCaretColumn(2);
  1179.  
  1180.             AcceptEvent(GLFW_KEY_DOWN);
  1181.             CHECK(te.GetCaret().line == 1);
  1182.             CHECK(te.GetCaret().column == 2);
  1183.         }
  1184.  
  1185.         SUBCASE("Caret column behavior: line clamping and desired column") {
  1186.             // Automatic length clamping behavior
  1187.             te.SetCaretLine(1);
  1188.             te.SetCaretColumn(22); // "This is another test l|i]ne"
  1189.  
  1190.             AcceptEvent(GLFW_KEY_UP);
  1191.             CHECK(te.GetCaret().line == 0);
  1192.             CHECK(te.GetCaret().column == 19);
  1193.  
  1194.             // "Desired column" behavior
  1195.             AcceptEvent(GLFW_KEY_DOWN);
  1196.             CHECK(te.GetCaret().line == 1);
  1197.             CHECK(te.GetCaret().column == 22);
  1198.         }
  1199.  
  1200.         SUBCASE("Move down at end of document does nothing") {
  1201.             te.SetCaretLine(1);
  1202.             te.SetCaretColumn(0);
  1203.  
  1204.             AcceptEvent(GLFW_KEY_DOWN);
  1205.             CHECK(te.GetCaret().line == 1);
  1206.             CHECK(te.GetCaret().column == 0);
  1207.         }
  1208.  
  1209.         SUBCASE("Move up at beginning of document does nothing") {
  1210.             te.SetCaretLine(0);
  1211.             te.SetCaretColumn(0);
  1212.  
  1213.             AcceptEvent(GLFW_KEY_UP);
  1214.             CHECK(te.GetCaret().line == 0);
  1215.             CHECK(te.GetCaret().column == 0);
  1216.         }
  1217.     }
  1218.  
  1219.     SUBCASE("scrollToFront()") {
  1220.         te.AppendLine("This is a test line");
  1221.         te.SetCaretLine(0);
  1222.         te.SetCaretColumn(5);
  1223.  
  1224.         te.ScrollToFront();
  1225.         CHECK(te.GetCaret().line == 0);
  1226.         CHECK(te.GetCaret().column == 0);
  1227.     }
  1228.  
  1229.     SUBCASE("scrollToBack()") {
  1230.         te.AppendLine("This is a test line");
  1231.         te.SetCaretLine(0);
  1232.         te.SetCaretColumn(0);
  1233.  
  1234.         te.ScrollToBack();
  1235.         CHECK(te.GetCaret().line == 0);
  1236.         CHECK(te.GetCaret().column == 19);
  1237.     }
  1238.  
  1239.     SUBCASE("Selection accessor") {
  1240.         te.AppendLine("This is a test line");
  1241.         te.AppendLine("This is another test line");
  1242.         te.SetSelection(TextSelection{
  1243.             .begin = CharLocation{ 0, 3 }, // Thi|s]
  1244.             .end = CharLocation{ 1, 10 }, // This is an|o]
  1245.         });
  1246.  
  1247.         SUBCASE("getSelectedText()") {
  1248.             CHECK(te.GetSelectedText() == "s is a test line\nThis is an");
  1249.         }
  1250.  
  1251.         SUBCASE("getSelectedLines()") {
  1252.             auto lines = te.GetSelectedLines();
  1253.             REQUIRE(lines.size() == 2);
  1254.             CHECK(lines[0] == "s is a test line\n");
  1255.             CHECK(lines[1] == "This is an");
  1256.         }
  1257.     }
  1258.  
  1259.     SUBCASE("Selection: shift+arrow key") {
  1260.         te.AppendLine("This is a test line");
  1261.         te.SetCaretLine(0);
  1262.         te.SetCaretColumn(3);
  1263.  
  1264.         // Presses shift+right 3 times
  1265.         AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
  1266.         AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
  1267.         AcceptEvent(GLFW_KEY_RIGHT, GLFW_MOD_SHIFT);
  1268.  
  1269.         SUBCASE("") {
  1270.             CHECK(te.HasSelection() == true);
  1271.             CHECK(te.GetSelectedText() == "s i");
  1272.         }
  1273.  
  1274.         SUBCASE("Moving caret removes selection") {
  1275.             AcceptEvent(GLFW_KEY_RIGHT, 0);
  1276.             CHECK(te.HasSelection() == false);
  1277.         }
  1278.     }
  1279. }
  1280.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement