// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details #include "CollabTextBox.hpp" #include "CollabTextBoxShaper.hpp" #include #include #include #include #include #include #include using namespace CollabTextBox; static inline SkRect offset(SkRect r, SkIPoint p) { return r.makeOffset((float)p.x(), (float)p.y()); } static constexpr SkRect kUnsetRect{-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX}; //static bool valid_utf8(const char* ptr, size_t size) { return SkUTF::CountUTF8(ptr, size) >= 0; } // Kind of like Python's readlines(), but without any allocation. // Calls f() on each line. // F is [](const char*, size_t) -> void template static void readlines(const void* data, size_t size, F f) { const char* start = (const char*)data; const char* end = start + size; const char* ptr = start; while (ptr < end) { while (*ptr++ != '\n' && ptr < end) {} size_t len = ptr - start; assert(len > 0); f(start, len); start = ptr; } } static std::string remove_newline(const char* str, size_t len) { return assert((str != nullptr) || (len == 0)), std::string(str, (len > 0 && str[len - 1] == '\n') ? len - 1 : len); } void Editor::markDirty(TextParagraph* line) { line->fBlob = nullptr; line->fShaped = false; line->fWordBoundaries = std::vector(); } void Editor::setFont(SkFont font) { if (font != fFont) { fFont = std::move(font); fNeedsReshape = true; for (auto& l : fLines) { this->markDirty(&l); } } } void Editor::setFontMgr(sk_sp fontMgr) { if(fontMgr != fFontMgr) { fFontMgr = fontMgr; fNeedsReshape = true; for (auto& l : fLines) { this->markDirty(&l); } } } void Editor::setWidth(int w) { if (fWidth != w) { fWidth = w; fNeedsReshape = true; for (auto& l : fLines) { this->markDirty(&l); } } } static SkPoint to_point(SkIPoint p) { return {(float)p.x(), (float)p.y()}; } TextPosition Editor::getPosition(SkIPoint xy) { // Clamp input coordinates to stop function from hanging xy.fX = std::clamp(xy.fX, 0, fWidth - 1); xy.fY = std::clamp(xy.fY, 0, fHeight - 1); TextPosition approximatePosition{0, 0}; this->reshapeAll(); for (size_t j = 0; j < fLines.size(); ++j) { const TextParagraph& line = fLines[j]; SkIRect lineRect = {0, line.fOrigin.y(), fWidth, j + 1 < fLines.size() ? fLines[j + 1].fOrigin.y() : INT_MAX}; if (const SkTextBlob* b = line.fBlob.get()) { SkIRect r = b->bounds().roundOut(); r.offset(line.fOrigin); lineRect.join(r); } if (!lineRect.contains(xy.x(), xy.y())) { continue; } SkPoint pt = to_point(xy - line.fOrigin); const std::vector& pos = line.fCursorPos; for (size_t i = 0; i < pos.size(); ++i) { if (pos[i] != kUnsetRect && pos[i].contains(pt.x(), pt.y())) { return TextPosition{i, j}; } } approximatePosition = {xy.x() <= line.fOrigin.x() ? 0 : line.fText.size(), j}; } return approximatePosition; } bool CollabTextBox::is_utf8_continuation(char v) { return ((unsigned char)v & 0b11000000) == 0b10000000; } const char* CollabTextBox::next_utf8(const char* p, const char* end) { if (p < end) { do { ++p; } while (p < end && is_utf8_continuation(*p)); } return p; } const char* CollabTextBox::align_utf8(const char* p, const char* begin) { while (p > begin && is_utf8_continuation(*p)) { --p; } return p; } const char* CollabTextBox::prev_utf8(const char* p, const char* begin) { return p > begin ? align_utf8(p - 1, begin) : begin; } SkRect Editor::getLocation(TextPosition cursor) { this->reshapeAll(); cursor = this->move(Movement::kNowhere, cursor); if (fLines.size() > 0) { const TextParagraph& cLine = fLines[cursor.fParagraphIndex]; SkRect pos = {0, 0, 0, 0}; if (cursor.fTextByteIndex < cLine.fCursorPos.size()) { pos = cLine.fCursorPos[cursor.fTextByteIndex]; } pos.fRight = pos.fLeft + 1; pos.fLeft -= 1; return offset(pos, cLine.fOrigin); } return SkRect{0, 0, 0, 0}; } static size_t count_char(const std::string& string, char value) { return std::ranges::count(string, value); } TextPosition Editor::insert(TextPosition pos, std::string_view utf8Text) { if (/*!valid_utf8(utf8Text, byteLen) ||*/ utf8Text.size() == 0) { // dont check for valid strings for now return pos; } pos = this->move(Movement::kNowhere, pos); fNeedsReshape = true; if (pos.fParagraphIndex < fLines.size()) { fLines[pos.fParagraphIndex].fText.insert(pos.fTextByteIndex, utf8Text.data(), utf8Text.size()); this->markDirty(&fLines[pos.fParagraphIndex]); } else { assert(pos.fParagraphIndex == fLines.size()); assert(pos.fTextByteIndex == 0); fLines.push_back(TextParagraph(utf8Text)); } pos = TextPosition{pos.fTextByteIndex + utf8Text.size(), pos.fParagraphIndex}; size_t newlinecount = count_char(fLines[pos.fParagraphIndex].fText, '\n'); if (newlinecount > 0) { std::string src = fLines[pos.fParagraphIndex].fText; std::vector::const_iterator next = fLines.begin() + pos.fParagraphIndex + 1; fLines.insert(next, newlinecount, TextParagraph()); TextParagraph* line = &fLines[pos.fParagraphIndex]; readlines(src.c_str(), src.size(), [&line](const char* str, size_t l) { (line++)->fText = remove_newline(str, l); }); } // Calculate the TextPosition post insert properly pos.fParagraphIndex = pos.fParagraphIndex + newlinecount; size_t lastNewline = utf8Text.find_last_of('\n'); if(std::string_view::npos != lastNewline) pos.fTextByteIndex = utf8Text.size() - lastNewline - 1; if(utf8Text == "\n") { // Special case where the string is just a newline pos.fTextByteIndex = 0; pos.fParagraphIndex--; } return pos; } TextPosition Editor::remove(TextPosition pos1, TextPosition pos2) { pos1 = this->move(Movement::kNowhere, pos1); pos2 = this->move(Movement::kNowhere, pos2); auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; }; TextPosition start = std::min(pos1, pos2, cmp); TextPosition end = std::max(pos1, pos2, cmp); if (start == end || start.fParagraphIndex == fLines.size()) { return start; } fNeedsReshape = true; if (start.fParagraphIndex == end.fParagraphIndex) { assert(end.fTextByteIndex > start.fTextByteIndex); fLines[start.fParagraphIndex].fText.erase(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex); this->markDirty(&fLines[start.fParagraphIndex]); } else { assert(end.fParagraphIndex < fLines.size()); auto& line = fLines[start.fParagraphIndex]; line.fText.erase(start.fTextByteIndex, line.fText.size() - start.fTextByteIndex); line.fText.insert(start.fTextByteIndex, fLines[end.fParagraphIndex].fText.substr(end.fTextByteIndex)); this->markDirty(&line); fLines.erase(fLines.begin() + start.fParagraphIndex + 1, fLines.begin() + end.fParagraphIndex + 1); } return start; } std::string Editor::get_string() const { std::string toRet; for(const TextParagraph& p : fLines) { toRet += p.fText; toRet += '\n'; } if(!toRet.empty()) toRet.pop_back(); return toRet; } std::string Editor::copy(TextPosition pos1, TextPosition pos2) const { pos1 = this->move(Movement::kNowhere, pos1); pos2 = this->move(Movement::kNowhere, pos2); auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; }; TextPosition start = std::min(pos1, pos2, cmp); TextPosition end = std::max(pos1, pos2, cmp); if(start == end || start.fParagraphIndex == fLines.size()) return {}; if(start.fParagraphIndex == end.fParagraphIndex) { assert(end.fTextByteIndex > start.fTextByteIndex); auto& str = fLines[start.fParagraphIndex].fText; return str.substr(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex); } assert(end.fParagraphIndex < fLines.size()); const std::vector::const_iterator firstP = fLines.begin() + start.fParagraphIndex; const std::vector::const_iterator lastP = fLines.begin() + end.fParagraphIndex; const auto& first = firstP->fText; const auto& last = lastP->fText; std::string toRet; toRet += first.substr(start.fTextByteIndex); for (auto line = firstP + 1; line < lastP; ++line) { toRet += '\n'; toRet += line->fText; } toRet += '\n'; toRet += last.substr(0, end.fTextByteIndex); return toRet; } static inline const char* begin(const std::string& s) { return s.c_str(); } static inline const char* end(const std::string& s) { return s.c_str() + s.size(); } static size_t align_column(const std::string& str, size_t p) { if(p >= str.size()) return str.size(); return align_utf8(begin(str) + p, begin(str)) - begin(str); } // returns smallest i such that list[i] > value. value > list[i-1] // Use a binary search since list is monotonic template static size_t find_first_larger(const std::vector& list, T value) { return (size_t)(std::upper_bound(list.begin(), list.end(), value) - list.begin()); } static size_t find_closest_x(const std::vector& bounds, float x, size_t b, size_t e) { if (b >= e) { return b; } assert(e <= bounds.size()); size_t best_index = b; float best_diff = ::fabsf(bounds[best_index].x() - x); for (size_t i = b + 1; i < e; ++i) { float d = ::fabsf(bounds[i].x() - x); if (d < best_diff) { best_diff = d; best_index = i; } } return best_index; } TextPosition Editor::move(Movement move, TextPosition pos) const { if (fLines.empty()) { return {0, 0}; } // First thing: fix possible bad input values. if (pos.fParagraphIndex >= fLines.size()) { pos.fParagraphIndex = fLines.size() - 1; pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size(); } else { pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex); } assert(pos.fParagraphIndex < fLines.size()); assert(pos.fTextByteIndex <= fLines[pos.fParagraphIndex].fText.size()); assert(pos.fTextByteIndex == fLines[pos.fParagraphIndex].fText.size() || !is_utf8_continuation(fLines[pos.fParagraphIndex].fText.begin()[pos.fTextByteIndex])); switch (move) { case Movement::kNowhere: break; case Movement::kLeft: if (0 == pos.fTextByteIndex) { if (pos.fParagraphIndex > 0) { --pos.fParagraphIndex; pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size(); } } else { const auto& str = fLines[pos.fParagraphIndex].fText; pos.fTextByteIndex = prev_utf8(begin(str) + pos.fTextByteIndex, begin(str)) - begin(str); } break; case Movement::kRight: if (fLines[pos.fParagraphIndex].fText.size() == pos.fTextByteIndex) { if (pos.fParagraphIndex + 1 < fLines.size()) { ++pos.fParagraphIndex; pos.fTextByteIndex = 0; } } else { const auto& str = fLines[pos.fParagraphIndex].fText; pos.fTextByteIndex = next_utf8(begin(str) + pos.fTextByteIndex, end(str)) - begin(str); } break; case Movement::kHome: { const std::vector& list = fLines[pos.fParagraphIndex].fLineEndOffsets; size_t f = find_first_larger(list, pos.fTextByteIndex); pos.fTextByteIndex = f > 0 ? list[f - 1] : 0; } break; case Movement::kEnd: { const std::vector& list = fLines[pos.fParagraphIndex].fLineEndOffsets; size_t f = find_first_larger(list, pos.fTextByteIndex); if (f < list.size()) { pos.fTextByteIndex = list[f] > 0 ? list[f] - 1 : 0; } else { pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size(); } } break; case Movement::kUp: { assert(pos.fTextByteIndex < fLines[pos.fParagraphIndex].fCursorPos.size()); float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left(); const std::vector& list = fLines[pos.fParagraphIndex].fLineEndOffsets; size_t f = find_first_larger(list, pos.fTextByteIndex); // list[f] > value. value > list[f-1] if (f > 0) { // not the first line in paragraph. pos.fTextByteIndex = find_closest_x(fLines[pos.fParagraphIndex].fCursorPos, x, (f == 1) ? 0 : list[f - 2], list[f - 1]); } else if (pos.fParagraphIndex > 0) { --pos.fParagraphIndex; const auto& newLine = fLines[pos.fParagraphIndex]; size_t r = newLine.fLineEndOffsets.size(); if (r > 0) { pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x, newLine.fLineEndOffsets[r - 1], newLine.fCursorPos.size()); } else { pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x, 0, newLine.fCursorPos.size()); } } pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex); } break; case Movement::kDown: { const std::vector& list = fLines[pos.fParagraphIndex].fLineEndOffsets; float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left(); size_t f = find_first_larger(list, pos.fTextByteIndex); if (f < list.size()) { const auto& bounds = fLines[pos.fParagraphIndex].fCursorPos; pos.fTextByteIndex = find_closest_x(bounds, x, list[f], f + 1 < list.size() ? list[f + 1] : bounds.size()); } else if (pos.fParagraphIndex + 1 < fLines.size()) { ++pos.fParagraphIndex; const auto& bounds = fLines[pos.fParagraphIndex].fCursorPos; const std::vector& l2 = fLines[pos.fParagraphIndex].fLineEndOffsets; pos.fTextByteIndex = find_closest_x(bounds, x, 0, l2.size() > 0 ? l2[0] : bounds.size()); } else { pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size(); } pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex); } break; case Movement::kWordLeft: { for(;;) { TextPosition prevCharPos = this->move(Movement::kLeft, pos); if(prevCharPos != TextPosition{0, 0} && prevCharPos.fTextByteIndex != fLines[prevCharPos.fParagraphIndex].fText.size() && std::isspace(fLines[prevCharPos.fParagraphIndex].fText[prevCharPos.fTextByteIndex])) pos = prevCharPos; else break; } if (pos.fTextByteIndex == 0) { pos = this->move(Movement::kLeft, pos); break; } for(;;) { TextPosition prevCharPos = this->move(Movement::kLeft, pos); const std::string& text = fLines[prevCharPos.fParagraphIndex].fText; if(prevCharPos == TextPosition{0, 0}) { pos = prevCharPos; break; } else if(prevCharPos.fTextByteIndex != text.size() && !std::isspace(text[prevCharPos.fTextByteIndex])) pos = prevCharPos; else break; } } break; case Movement::kWordRight: { const std::string& text = fLines[pos.fParagraphIndex].fText; if(pos.fTextByteIndex == text.size()) pos = this->move(Movement::kRight, pos); else { for(;;) { const std::string& text = fLines[pos.fParagraphIndex].fText; if(pos.fTextByteIndex != text.size() && !std::isspace(text[pos.fTextByteIndex])) pos = this->move(Movement::kRight, pos); else break; } } for(;;) { const std::string& text = fLines[pos.fParagraphIndex].fText; if(pos.fTextByteIndex != text.size() && std::isspace(text[pos.fTextByteIndex])) pos = this->move(Movement::kRight, pos); else break; } } break; } return pos; } void Editor::paint(SkCanvas* c, PaintOpts options) { this->reshapeAll(); if(!c) return; c->drawPaint(SkPaint(options.fBackgroundColor)); if(options.showCursor) { auto& cur = options.cursor; SkPaint selection = SkPaint(SkColor4f{options.cursorColor[0], options.cursorColor[1], options.cursorColor[2], 0.3f}); auto cmp = [](const TextPosition& u, const TextPosition& v) { return u < v; }; for(TextPosition pos = std::min(cur.selectionBeginPos, cur.selectionEndPos, cmp), end = std::max(cur.selectionBeginPos, cur.selectionEndPos, cmp); pos < end; pos = this->move(Movement::kRight, pos)) { assert(pos.fParagraphIndex < fLines.size()); const TextParagraph& l = fLines[pos.fParagraphIndex]; c->drawRect(offset(l.fCursorPos[pos.fTextByteIndex], l.fOrigin), selection); } if(fLines.size() > 0) c->drawRect(Editor::getLocation(cur.pos), SkPaint(options.cursorColor)); } SkPaint foreground = SkPaint(options.fForegroundColor); for(const TextParagraph& line : fLines) { if(line.fBlob) c->drawTextBlob(line.fBlob.get(), line.fOrigin.x(), line.fOrigin.y(), foreground); } } void Editor::reshapeAll() { if(fNeedsReshape && fFontMgr) { // fFontMgr must not be null for text to be reshaped if(fLines.empty()) fLines.push_back(TextParagraph()); float shape_width = (float)(fWidth); for(TextParagraph& line : fLines) { if(!line.fShaped) { ShapeResult result = shape(line.fText.c_str(), line.fText.size(), fFont, fFontMgr, fLocale, shape_width); line.fBlob = std::move(result.blob); line.fLineEndOffsets = std::move(result.lineBreakOffsets); line.fCursorPos = std::move(result.glyphBounds); line.fWordBoundaries = std::move(result.wordBreaks); line.fHeight = result.verticalAdvance; line.fShaped = true; } } int y = 0; for (TextParagraph& line : fLines) { line.fOrigin = {0, y}; y += line.fHeight; } fHeight = y; fNeedsReshape = false; } }