hnOsmium0001

UI/TextEdit WIP v1

Jan 31st, 2021
872
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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.  
RAW Paste Data

Adblocker detected! Please consider disabling it...

We've detected AdBlock Plus or some other adblocking software preventing Pastebin.com from fully loading.

We don't have any obnoxious sound, or popup ads, we actively block these annoying types of ads!

Please add Pastebin.com to your ad blocker whitelist or disable your adblocking software.

×