From 295529586c5fa601169d3db406e19f838e4629a5 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Sat, 18 Oct 2025 13:32:12 +0300 Subject: [PATCH] Textbox mostly works with text other than ASCII --- src/MainProgram.cpp | 20 +- src/RichTextBox/RichTextBox.cpp | 402 +++++++++++++++++++++++++++----- src/RichTextBox/RichTextBox.hpp | 44 +++- 3 files changed, 383 insertions(+), 83 deletions(-) diff --git a/src/MainProgram.cpp b/src/MainProgram.cpp index 8353f6f..2e9942c 100755 --- a/src/MainProgram.cpp +++ b/src/MainProgram.cpp @@ -71,26 +71,16 @@ MainProgram::MainProgram(): textBox = std::make_shared(); textBoxCursor = std::make_shared(); - textBox->set_width(1000.0f); textBox->set_font_collection(fonts.collection); - textBox->insert(0, R"( -Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - -Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - -Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - -Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - -Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. - )"); - + textBox->insert({0, 0}, R"(Hello world)"); paintOpts.cursor = RichTextBox::Cursor{}; - paintOpts.cursorColor = {1.0f, 0.0f, 0.0f, 0.7f}; + paintOpts.cursorColor = {1.0f, 0.0f, 0.0f}; } void MainProgram::update() { input.text.set_rich_text_box_input(textBox, textBoxCursor); + textBox->process_mouse_left_button(*textBoxCursor, input.mouse.pos - Vector2f{10.0f, 10.0f}, input.mouse.leftClicks, input.mouse.leftDown, input.key(InputManager::KEY_GENERIC_LSHIFT).held); + textBox->set_width(window.size.x() - 20.0f); //while(std::chrono::duration_cast>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit); //lastFrameTime = std::chrono::steady_clock::now(); @@ -273,7 +263,7 @@ void MainProgram::draw(SkCanvas* canvas) { canvas->clear(SkColor4f{0.0f, 0.0f, 0.0f, 1.0f}); canvas->save(); - canvas->translate(200.0f, 200.0f); + canvas->translate(10.0f, 10.0f); paintOpts.cursor = *textBoxCursor; textBox->paint(canvas, paintOpts); canvas->restore(); diff --git a/src/RichTextBox/RichTextBox.cpp b/src/RichTextBox/RichTextBox.cpp index 601aa66..09e7074 100644 --- a/src/RichTextBox/RichTextBox.cpp +++ b/src/RichTextBox/RichTextBox.cpp @@ -1,8 +1,17 @@ #include "RichTextBox.hpp" +#include +#include +#include +#include #include #include #include #include +#include + +bool RichTextBox::TextPosition::operator<(const RichTextBox::TextPosition& o) const { + return (this->fParagraphIndex < o.fParagraphIndex) || (this->fParagraphIndex == o.fParagraphIndex && this->fTextByteIndex < o.fTextByteIndex); +} RichTextBox::RichTextBox() { } @@ -12,19 +21,19 @@ void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool sh switch(in) { case InputKey::LEFT: - cur.pos = cur.selectionBeginPos = move(Movement::LEFT, cur.pos); + cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::LEFT_WORD : Movement::LEFT, cur.pos); moved = true; break; case InputKey::RIGHT: - cur.pos = cur.selectionBeginPos = move(Movement::RIGHT, cur.pos); + cur.pos = cur.selectionBeginPos = move(ctrl ? Movement::RIGHT_WORD : Movement::RIGHT, cur.pos); moved = true; break; case InputKey::UP: - cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos); + cur.pos = cur.selectionBeginPos = move(Movement::UP, cur.pos, &cur.previousX); moved = true; break; case InputKey::DOWN: - cur.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos); + cur.pos = cur.selectionBeginPos = move(Movement::DOWN, cur.pos, &cur.previousX); moved = true; break; case InputKey::HOME: @@ -52,25 +61,45 @@ void RichTextBox::process_key_input(Cursor& cur, InputKey in, bool ctrl, bool sh process_text_input(cur, "\n"); break; case InputKey::SELECT_ALL: - cur.selectionBeginPos = 0; - cur.pos = cur.selectionEndPos = text.size(); + cur.selectionBeginPos = {0, 0}; + cur.pos = cur.selectionEndPos = {text.size(), 0}; break; } if(moved && !shift) cur.selectionEndPos = cur.selectionBeginPos; + + if(in != InputKey::UP && in != InputKey::DOWN) + cur.previousX = std::nullopt; +} + +void RichTextBox::process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift) { + if(clicked || held) { + rebuild(); + + std::optional newPos = get_text_pos_closest_to_point(pos); + if(newPos.has_value()) { + cur.pos = newPos.value(); + cur.selectionBeginPos = cur.pos; + if(clicked && !shift) + cur.selectionEndPos = cur.selectionBeginPos; + cur.previousX = std::nullopt; + } + } } std::string RichTextBox::process_copy(Cursor& cur) { - uint64_t trueBegin = std::min(cur.selectionBeginPos, cur.selectionEndPos); - uint64_t trueEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos); - return text.substr(trueBegin, trueEnd - trueBegin); + TextPosition trueBegin = std::min(cur.selectionBeginPos, cur.selectionEndPos); + TextPosition trueEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos); + return text.substr(trueBegin.fTextByteIndex, trueEnd.fTextByteIndex - trueBegin.fTextByteIndex); } std::string RichTextBox::process_cut(Cursor& cur) { std::string toRet = process_copy(cur); - if(cur.selectionBeginPos != cur.selectionEndPos) + if(cur.selectionBeginPos != cur.selectionEndPos) { cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); + cur.previousX = std::nullopt; + } return toRet; } @@ -79,41 +108,175 @@ void RichTextBox::process_text_input(Cursor& cur, const std::string& in) { if(cur.selectionBeginPos != cur.selectionEndPos) cur.selectionEndPos = cur.selectionBeginPos = cur.pos = remove(cur.selectionBeginPos, cur.selectionEndPos); cur.selectionEndPos = cur.selectionBeginPos = cur.pos = insert(cur.pos, in); + cur.previousX = std::nullopt; } } -uint64_t RichTextBox::move(Movement movement, uint64_t pos) { +size_t RichTextBox::count_grapheme() { + rebuild(); + auto u = SkUnicodes::ICU::Make(); + auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes); + breakIterator->setText(text.c_str(), text.size()); + size_t count = 0; + while(!breakIterator->isDone()) { + breakIterator->next(); + count++; + } + return count - 1; +} + +size_t RichTextBox::next_grapheme(size_t textBytePos) { + rebuild(); + if(textBytePos == text.size()) + return text.size(); + auto u = SkUnicodes::ICU::Make(); + auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kGraphemes); + breakIterator->setText(text.c_str() + textBytePos, text.size() - textBytePos); + size_t count = breakIterator->next(); + return count + textBytePos; +} + +size_t RichTextBox::prev_grapheme(size_t textBytePos) { + rebuild(); + if(textBytePos == 0) + return 0; + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + bool glyphClusterFound = paragraph->getGlyphClusterAt(textBytePos - 1, &glyphInfo); + if(glyphClusterFound) + return glyphInfo.fClusterTextRange.start; + return std::numeric_limits::max(); +} + +std::optional RichTextBox::get_text_pos_closest_to_point(const Vector2f& point) { + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + bool foundGlyph = paragraph->getClosestGlyphClusterAt(point.x(), point.y(), &glyphInfo); + if(foundGlyph) { + TextPosition toRet = {0, 0}; + toRet.fTextByteIndex = glyphInfo.fClusterTextRange.start; + if(std::abs(glyphInfo.fBounds.right() - point.x()) < std::abs(glyphInfo.fBounds.left() - point.x())) + toRet.fTextByteIndex = next_grapheme(toRet.fTextByteIndex); + return toRet; + } + return std::nullopt; +} + +RichTextBox::TextPosition RichTextBox::move(Movement movement, TextPosition pos, std::optional* previousX) { rebuild(); - if(pos >= text.size()) - pos = text.size(); + if(pos.fTextByteIndex >= text.size()) + pos.fTextByteIndex = text.size(); switch (movement) { - case Movement::LEFT: - if(pos != 0) - pos--; + case Movement::LEFT: { + pos.fTextByteIndex = prev_grapheme(pos.fTextByteIndex); break; - case Movement::RIGHT: - if(pos < text.size()) - pos++; + } + case Movement::RIGHT: { + pos.fTextByteIndex = next_grapheme(pos.fTextByteIndex); break; + } case Movement::UP: { - SkRect cursorRect = get_cursor_rect(pos); - Vector2f pointToCheck{cursorRect.x(), cursorRect.top() - 0.1f}; - pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position; + assert(previousX != nullptr); + if(text.size() == 0) + break; + if(pos.fTextByteIndex == 0) + break; + int lineNumber = paragraph->getLineNumberAt(prev_grapheme(pos.fTextByteIndex)); + if(lineNumber == -1) + break; + if(lineNumber == 0) { + pos.fTextByteIndex = 0; + if(previousX != nullptr) + *previousX = get_cursor_rect(pos).centerX(); + } + else { + skia::textlayout::LineMetrics prevLineMetrics; + paragraph->getLineMetricsAt(lineNumber - 1, &prevLineMetrics); + SkRect cursorRect = get_cursor_rect(pos); + Vector2f pointToCheck = {(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), prevLineMetrics.fBaseline}; + + std::optional newPos = get_text_pos_closest_to_point(pointToCheck); + if(newPos.has_value()) + pos = newPos.value(); + if(previousX != nullptr && !previousX->has_value()) { + *previousX = pointToCheck.x(); + } + } break; } case Movement::DOWN: { - SkRect cursorRect = get_cursor_rect(pos); - Vector2f pointToCheck{cursorRect.x(), cursorRect.bottom() + 0.1f}; - pos = paragraph->getGlyphPositionAtCoordinate(pointToCheck.x(), pointToCheck.y()).position; + assert(previousX != nullptr); + if(text.size() == 0) + break; + if(pos.fTextByteIndex == text.size()) + break; + int lineNumber = paragraph->getLineNumberAt(pos.fTextByteIndex); + if(lineNumber == -1) + break; + if(lineNumber == static_cast(paragraph->lineNumber() - 1)) { + pos.fTextByteIndex = text.size(); + if(previousX != nullptr) + *previousX = get_cursor_rect(pos).centerX(); + } + else { + skia::textlayout::LineMetrics nextLineMetrics; + paragraph->getLineMetricsAt(lineNumber + 1, &nextLineMetrics); + SkRect cursorRect = get_cursor_rect(pos); + Vector2f pointToCheck{(previousX != nullptr && previousX->has_value()) ? previousX->value() : cursorRect.centerX(), nextLineMetrics.fBaseline}; + + std::optional newPos = get_text_pos_closest_to_point(pointToCheck); + if(newPos.has_value()) + pos = newPos.value(); + if(previousX != nullptr && !previousX->has_value()) { + *previousX = pointToCheck.x(); + } + } + break; + } + case Movement::LEFT_WORD: { + if(pos.fTextByteIndex == 0) + break; + auto u = SkUnicodes::ICU::Make(); + + size_t byteIndexToRet = 0; + + auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords); + breakIterator->setText(text.c_str(), text.size()); + + while(!breakIterator->isDone()) { + size_t nextByteIndex = breakIterator->next(); + if(nextByteIndex >= pos.fTextByteIndex) + return {byteIndexToRet, 0}; + + const char* tPtr = text.c_str() + nextByteIndex; + SkUnichar u = SkUTF::NextUTF8(&tPtr, text.c_str() + text.size()); + if(!SkUnicodes::ICU::Make()->isWhitespace(u)) + byteIndexToRet = nextByteIndex; + } + break; + } + case Movement::RIGHT_WORD: { + if(pos.fTextByteIndex == text.size()) + return pos; + auto u = SkUnicodes::ICU::Make(); + auto breakIterator = u->makeBreakIterator(SkUnicode::BreakType::kWords); + breakIterator->setText(text.c_str() + pos.fTextByteIndex, text.size() - pos.fTextByteIndex); + while(!breakIterator->isDone()) { + TextPosition p = {breakIterator->next() + pos.fTextByteIndex, 0}; + if(p.fTextByteIndex == text.size()) + return p; + const char* tPtr = text.c_str() + p.fTextByteIndex; + SkUnichar u = SkUTF::NextUTF8(&tPtr, text.c_str() + text.size()); + if(!SkUnicodes::ICU::Make()->isWhitespace(u)) + return p; + } break; } case Movement::HOME: - pos = 0; + pos.fTextByteIndex = 0; break; case Movement::END: - pos = text.size(); + pos.fTextByteIndex = text.size(); break; } return pos; @@ -121,8 +284,10 @@ uint64_t RichTextBox::move(Movement movement, uint64_t pos) { } void RichTextBox::set_width(float newWidth) { - width = newWidth; - needsRebuild = true; + if(width != newWidth) { + width = newWidth; + needsRebuild = true; + } } void RichTextBox::set_allow_newlines(bool allow) { @@ -138,6 +303,7 @@ void RichTextBox::rebuild() { if(needsRebuild) { skia::textlayout::ParagraphStyle pStyle; pStyle.setTextAlign(skia::textlayout::TextAlign::kLeft); + pStyle.setTextDirection(skia::textlayout::TextDirection::kLtr); skia::textlayout::TextStyle tStyle; tStyle.setFontSize(20.0f); tStyle.setFontFamilies({SkString{"Roboto"}}); @@ -147,25 +313,26 @@ void RichTextBox::rebuild() { a.addText(text.c_str(), text.length()); paragraph = a.Build(); paragraph->layout(width); + needsRebuild = false; } } -uint64_t RichTextBox::insert(uint64_t pos, const std::string& textToInsert) { - text.insert(pos, textToInsert); +RichTextBox::TextPosition RichTextBox::insert(TextPosition pos, const std::string& textToInsert) { + text.insert(pos.fTextByteIndex, textToInsert); needsRebuild = true; - return pos + textToInsert.size(); + return TextPosition{pos.fTextByteIndex + textToInsert.size()}; } -uint64_t RichTextBox::remove(uint64_t start, uint64_t end) { - if(start > end) - std::swap(start, end); +RichTextBox::TextPosition RichTextBox::remove(TextPosition start, TextPosition end) { + TextPosition trueBegin = std::min(start, end); + TextPosition trueEnd = std::max(start, end); - text.erase(start, end - start); + text.erase(trueBegin.fTextByteIndex, trueEnd.fTextByteIndex - trueBegin.fTextByteIndex); needsRebuild = true; - return start; + return trueBegin; } -SkRect RichTextBox::get_cursor_rect(uint64_t pos) { +SkRect RichTextBox::get_cursor_rect(TextPosition pos) { SkPoint topLeft; float height; @@ -173,41 +340,158 @@ SkRect RichTextBox::get_cursor_rect(uint64_t pos) { topLeft = {0.0f, 0.0f}; height = paragraph->getHeight(); } - else if(pos == text.size()) { - std::vector posBoxes = paragraph->getRectsForRange(pos - 1, pos, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax); - if(text.back() == '\n') { - topLeft = {0.0f, posBoxes.back().rect.bottom()}; - height = std::abs(paragraph->getHeight() - topLeft.y()); + else { + if(pos.fTextByteIndex == text.size()) { + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + bool exists = paragraph->getGlyphClusterAt(prev_grapheme(pos.fTextByteIndex), &glyphInfo); + if(exists) { + topLeft = glyphInfo.fBounds.TR(); + height = glyphInfo.fBounds.height(); + } + else { + topLeft = {0.0f, 0.0f}; + height = 0.0f; + } } else { - topLeft = posBoxes.front().rect.TR(); - height = posBoxes.front().rect.height(); + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + bool exists = paragraph->getGlyphClusterAt(pos.fTextByteIndex, &glyphInfo); + if(exists) { + topLeft = glyphInfo.fBounds.TL(); + height = glyphInfo.fBounds.height(); + } + else { + topLeft = {0.0f, 0.0f}; + height = 0.0f; + } } } - else { - std::vector posBoxes = paragraph->getRectsForRange(pos, pos + 1, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax); - topLeft = posBoxes.front().rect.TL(); - height = posBoxes.front().rect.height(); - } return SkRect::MakeXYWH(topLeft.x(), topLeft.y(), 3.0f, height); } +std::vector RichTextBox::rects_between_text_positions(TextPosition start, TextPosition end) { + TextPosition trueStart = std::min(start, end); + TextPosition trueEnd = std::max(start, end); + + unsigned glyphID1 = 0, glyphID2 = 0; + + // Commenting this so that illegal instructions become more apparent (they're there even if its uncommented, but can't be caught as easily) + //if(trueStart == trueEnd) + // return {}; + + if(text.empty()) + return {}; + else { + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + + if(trueStart.fTextByteIndex == text.size()) { + if(paragraph->getGlyphClusterAt(prev_grapheme(trueStart.fTextByteIndex), &glyphInfo)) { + glyphID1 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position + 1; + } + } + else { + if(paragraph->getGlyphClusterAt(trueStart.fTextByteIndex, &glyphInfo)) + glyphID1 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position; + } + + if(trueEnd.fTextByteIndex == text.size()) { + if(paragraph->getGlyphClusterAt(prev_grapheme(trueEnd.fTextByteIndex), &glyphInfo)) + glyphID2 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position + 1; + } + else { + if(paragraph->getGlyphClusterAt(trueEnd.fTextByteIndex, &glyphInfo)) + glyphID2 = paragraph->getGlyphPositionAtCoordinate(glyphInfo.fBounds.x(), glyphInfo.fBounds.centerY()).position; + } + } + + if(glyphID1 == glyphID2) + return {}; + + return paragraph->getRectsForRange(glyphID1, glyphID2, skia::textlayout::RectHeightStyle::kMax, skia::textlayout::RectWidthStyle::kMax); +} + +constexpr float SELECTION_RECT_EXTRA_AREA = 2.0f; + +std::vector RichTextBox::rects_between_text_positions_2(TextPosition start, TextPosition end) { + TextPosition trueStart = std::min(start, end); + TextPosition trueEnd = std::max(start, end); + + std::vector> lineRects; + + if(text.empty()) + return {}; + else { + for(size_t i = trueStart.fTextByteIndex; i < trueEnd.fTextByteIndex; i++) { + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + if(paragraph->getGlyphClusterAt(i, &glyphInfo)) { + int lineNumber = paragraph->getLineNumberAt(i); + if(lineNumber == -1) + break; + skia::textlayout::LineMetrics lineMetrics; + if(paragraph->getLineMetricsAt(lineNumber, &lineMetrics)) { + SkRect r = SkRect::MakeLTRB(glyphInfo.fBounds.left(), lineMetrics.fBaseline - lineMetrics.fAscent, glyphInfo.fBounds.right(), lineMetrics.fBaseline + lineMetrics.fDescent); + auto it = std::find_if(lineRects.begin(), lineRects.end(), [&](const auto& p) { + SkRect checkCollideWith = p.second; + checkCollideWith.fLeft = checkCollideWith.fLeft - SELECTION_RECT_EXTRA_AREA; + checkCollideWith.fRight = checkCollideWith.fRight + SELECTION_RECT_EXTRA_AREA; + return p.first == lineNumber && checkCollideWith.intersects(r); + }); + if(it != lineRects.end()) + it->second.join(r); + else + lineRects.emplace_back(lineNumber, r); + } + } + } + } + + std::vector toRet; + for(auto& l : lineRects) + toRet.emplace_back(l.second); + + return toRet; +} + +void RichTextBox::rects_between_text_positions_func(TextPosition start, TextPosition end, std::function f) { + TextPosition trueStart = std::min(start, end); + TextPosition trueEnd = std::max(start, end); + + if(text.empty()) + return; + else { + for(size_t i = trueStart.fTextByteIndex; i < trueEnd.fTextByteIndex; i++) { + skia::textlayout::Paragraph::GlyphClusterInfo glyphInfo; + if(paragraph->getGlyphClusterAt(i, &glyphInfo)) { + int lineNumber = paragraph->getLineNumberAt(i); + if(lineNumber == -1) + break; + skia::textlayout::LineMetrics lineMetrics; + if(paragraph->getLineMetricsAt(lineNumber, &lineMetrics)) { + SkRect r = SkRect::MakeLTRB(glyphInfo.fBounds.left() - SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline - lineMetrics.fAscent - SELECTION_RECT_EXTRA_AREA, glyphInfo.fBounds.right() + SELECTION_RECT_EXTRA_AREA, lineMetrics.fBaseline + lineMetrics.fDescent + SELECTION_RECT_EXTRA_AREA); + f(r); + } + } + } + } +} + + void RichTextBox::paint(SkCanvas* canvas, const PaintOpts& paintOpts) { rebuild(); if(paintOpts.cursor.has_value()) { auto& cur = paintOpts.cursor.value(); - SkPaint selection{color_mul_alpha(paintOpts.cursorColor, 0.6f)}; - uint64_t trueSelectionStart = std::min(cur.selectionBeginPos, cur.selectionEndPos); - uint64_t trueSelectionEnd = std::max(cur.selectionBeginPos, cur.selectionEndPos); - std::vector selectionBoxes = paragraph->getRectsForRange(trueSelectionStart, trueSelectionEnd, skia::textlayout::RectHeightStyle::kTight, skia::textlayout::RectWidthStyle::kTight); - for(auto& selectionBox : selectionBoxes) { - SkRect rectFixSmallOverlap = SkRect::MakeXYWH(selectionBox.rect.x(), selectionBox.rect.y(), selectionBox.rect.width(), std::max(selectionBox.rect.height() - 0.25f, 0.0f)); - canvas->drawRect(rectFixSmallOverlap, selection); + SkPaint p{SkColor4f{paintOpts.cursorColor.x(), paintOpts.cursorColor.y(), paintOpts.cursorColor.z(), 1.0f}}; + if(cur.selectionBeginPos != cur.selectionEndPos) { + canvas->saveLayerAlphaf(nullptr, 0.6f); + rects_between_text_positions_func(cur.selectionBeginPos, cur.selectionEndPos, [&](const SkRect& rect) { + canvas->drawRect(rect, p); + }); + canvas->restore(); } - if(cur.pos <= text.size()) { - canvas->drawRect(get_cursor_rect(cur.pos), SkPaint{paintOpts.cursorColor}); + if(cur.pos.fTextByteIndex <= text.size()) { + canvas->drawRect(get_cursor_rect(cur.pos), p); } } diff --git a/src/RichTextBox/RichTextBox.hpp b/src/RichTextBox/RichTextBox.hpp index 385d753..b954db7 100644 --- a/src/RichTextBox/RichTextBox.hpp +++ b/src/RichTextBox/RichTextBox.hpp @@ -14,25 +14,39 @@ class RichTextBox { RIGHT, UP, DOWN, + LEFT_WORD, + RIGHT_WORD, HOME, END }; + struct TextPosition { + // Set to uint32_t values because theyll be saved and sent over networks. Size has to be consistent + size_t fTextByteIndex = std::numeric_limits::max(); + size_t fParagraphIndex = std::numeric_limits::max(); + template void serialize(Archive& a) { + a((uint32_t)fTextByteIndex, (uint32_t)fParagraphIndex); + } + bool operator==(const TextPosition& o) const = default; + bool operator<(const TextPosition& o) const; + }; + struct Cursor { - uint64_t pos = 0; - uint64_t selectionBeginPos = 0; - uint64_t selectionEndPos = 0; + TextPosition pos = {0, 0}; + TextPosition selectionBeginPos = {0, 0}; + TextPosition selectionEndPos = {0, 0}; + std::optional previousX; }; struct PaintOpts { - SkColor4f cursorColor = {1, 0, 0, 1}; + Vector3f cursorColor = {1, 0, 0}; std::optional cursor; }; - uint64_t move(Movement movement, uint64_t pos); + TextPosition move(Movement movement, TextPosition pos, std::optional* previousX = nullptr); void set_width(float newWidth); - uint64_t insert(uint64_t pos, const std::string& textToInsert); - uint64_t remove(uint64_t start, uint64_t end); + TextPosition insert(TextPosition pos, const std::string& textToInsert); + TextPosition remove(TextPosition start, TextPosition end); void set_font_collection(const sk_sp& fC); void paint(SkCanvas* canvas, const PaintOpts& paintOpts); void set_allow_newlines(bool allow); @@ -50,17 +64,29 @@ class RichTextBox { SELECT_ALL }; + void process_mouse_left_button(Cursor& cur, const Vector2f& pos, bool clicked, bool held, bool shift); void process_key_input(Cursor& cur, InputKey in, bool ctrl, bool shift); std::string process_copy(Cursor& cur); std::string process_cut(Cursor& cur); + void process_text_input(Cursor& cur, const std::string& in); private: - SkRect get_cursor_rect(uint64_t pos); + size_t count_grapheme(); + size_t next_grapheme(size_t textBytePos); + size_t prev_grapheme(size_t textBytePos); + + std::vector rects_between_text_positions(TextPosition start, TextPosition end); + std::vector rects_between_text_positions_2(TextPosition start, TextPosition end); + void rects_between_text_positions_func(TextPosition start, TextPosition end, std::function f); + + SkRect get_cursor_rect(TextPosition pos); + + std::optional get_text_pos_closest_to_point(const Vector2f& point); void rebuild(); bool newlinesAllowed = true; - float width = 100.0f; + float width = 0.0f; float fontSize = 0.0f; bool needsRebuild = true; sk_sp fontCollection;