| // Copyright 2019 Google LLC. | |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | |
| #include "modules/skplaintexteditor/include/editor.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkExecutor.h" | |
| #include "include/core/SkPath.h" | |
| #include "src/base/SkUTF.h" | |
| #include "modules/skplaintexteditor/src/shape.h" | |
| #include <algorithm> | |
| #include <cfloat> | |
| using namespace SkPlainTextEditor; | |
| 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 <typename F> | |
| 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; | |
| SkASSERT(len > 0); | |
| f(start, len); | |
| start = ptr; | |
| } | |
| } | |
| static StringSlice remove_newline(const char* str, size_t len) { | |
| return SkASSERT((str != nullptr) || (len == 0)), | |
| StringSlice(str, (len > 0 && str[len - 1] == '\n') ? len - 1 : len); | |
| } | |
| void Editor::markDirty(TextLine* line) { | |
| line->fBlob = nullptr; | |
| line->fShaped = false; | |
| line->fWordBoundaries = std::vector<bool>(); | |
| } | |
| 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<SkFontMgr> fontMgr) { | |
| 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()}; } | |
| Editor::TextPosition Editor::getPosition(SkIPoint xy) { | |
| Editor::TextPosition approximatePosition; | |
| this->reshapeAll(); | |
| for (size_t j = 0; j < fLines.size(); ++j) { | |
| const TextLine& 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<SkRect>& pos = line.fCursorPos; | |
| for (size_t i = 0; i < pos.size(); ++i) { | |
| if (pos[i] != kUnsetRect && pos[i].contains(pt.x(), pt.y())) { | |
| return Editor::TextPosition{i, j}; | |
| } | |
| } | |
| approximatePosition = {xy.x() <= line.fOrigin.x() ? 0 : line.fText.size(), j}; | |
| } | |
| return approximatePosition; | |
| } | |
| static inline bool is_utf8_continuation(char v) { | |
| return ((unsigned char)v & 0b11000000) == | |
| 0b10000000; | |
| } | |
| static const char* next_utf8(const char* p, const char* end) { | |
| if (p < end) { | |
| do { | |
| ++p; | |
| } while (p < end && is_utf8_continuation(*p)); | |
| } | |
| return p; | |
| } | |
| static const char* align_utf8(const char* p, const char* begin) { | |
| while (p > begin && is_utf8_continuation(*p)) { | |
| --p; | |
| } | |
| return p; | |
| } | |
| static const char* prev_utf8(const char* p, const char* begin) { | |
| return p > begin ? align_utf8(p - 1, begin) : begin; | |
| } | |
| SkRect Editor::getLocation(Editor::TextPosition cursor) { | |
| this->reshapeAll(); | |
| cursor = this->move(Editor::Movement::kNowhere, cursor); | |
| if (fLines.size() > 0) { | |
| const TextLine& 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 StringSlice& string, char value) { | |
| size_t count = 0; | |
| for (char c : string) { if (c == value) { ++count; } } | |
| return count; | |
| } | |
| Editor::TextPosition Editor::insert(TextPosition pos, const char* utf8Text, size_t byteLen) { | |
| if (!valid_utf8(utf8Text, byteLen) || 0 == byteLen) { | |
| return pos; | |
| } | |
| pos = this->move(Editor::Movement::kNowhere, pos); | |
| fNeedsReshape = true; | |
| if (pos.fParagraphIndex < fLines.size()) { | |
| fLines[pos.fParagraphIndex].fText.insert(pos.fTextByteIndex, utf8Text, byteLen); | |
| this->markDirty(&fLines[pos.fParagraphIndex]); | |
| } else { | |
| SkASSERT(pos.fParagraphIndex == fLines.size()); | |
| SkASSERT(pos.fTextByteIndex == 0); | |
| fLines.push_back(Editor::TextLine(StringSlice(utf8Text, byteLen))); | |
| } | |
| pos = Editor::TextPosition{pos.fTextByteIndex + byteLen, pos.fParagraphIndex}; | |
| size_t newlinecount = count_char(fLines[pos.fParagraphIndex].fText, '\n'); | |
| if (newlinecount > 0) { | |
| StringSlice src = std::move(fLines[pos.fParagraphIndex].fText); | |
| std::vector<TextLine>::const_iterator next = fLines.begin() + pos.fParagraphIndex + 1; | |
| fLines.insert(next, newlinecount, TextLine()); | |
| TextLine* line = &fLines[pos.fParagraphIndex]; | |
| readlines(src.begin(), src.size(), [&line](const char* str, size_t l) { | |
| (line++)->fText = remove_newline(str, l); | |
| }); | |
| } | |
| return pos; | |
| } | |
| Editor::TextPosition Editor::remove(TextPosition pos1, TextPosition pos2) { | |
| pos1 = this->move(Editor::Movement::kNowhere, pos1); | |
| pos2 = this->move(Editor::Movement::kNowhere, pos2); | |
| auto cmp = [](const Editor::TextPosition& u, const Editor::TextPosition& v) { return u < v; }; | |
| Editor::TextPosition start = std::min(pos1, pos2, cmp); | |
| Editor::TextPosition end = std::max(pos1, pos2, cmp); | |
| if (start == end || start.fParagraphIndex == fLines.size()) { | |
| return start; | |
| } | |
| fNeedsReshape = true; | |
| if (start.fParagraphIndex == end.fParagraphIndex) { | |
| SkASSERT(end.fTextByteIndex > start.fTextByteIndex); | |
| fLines[start.fParagraphIndex].fText.remove( | |
| start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex); | |
| this->markDirty(&fLines[start.fParagraphIndex]); | |
| } else { | |
| SkASSERT(end.fParagraphIndex < fLines.size()); | |
| auto& line = fLines[start.fParagraphIndex]; | |
| line.fText.remove(start.fTextByteIndex, | |
| line.fText.size() - start.fTextByteIndex); | |
| line.fText.insert(start.fTextByteIndex, | |
| fLines[end.fParagraphIndex].fText.begin() + end.fTextByteIndex, | |
| fLines[end.fParagraphIndex].fText.size() - end.fTextByteIndex); | |
| this->markDirty(&line); | |
| fLines.erase(fLines.begin() + start.fParagraphIndex + 1, | |
| fLines.begin() + end.fParagraphIndex + 1); | |
| } | |
| return start; | |
| } | |
| static void append(char** dst, size_t* count, const char* src, size_t n) { | |
| if (*dst) { | |
| ::memcpy(*dst, src, n); | |
| *dst += n; | |
| } | |
| *count += n; | |
| } | |
| size_t Editor::copy(TextPosition pos1, TextPosition pos2, char* dst) const { | |
| size_t size = 0; | |
| pos1 = this->move(Editor::Movement::kNowhere, pos1); | |
| pos2 = this->move(Editor::Movement::kNowhere, pos2); | |
| auto cmp = [](const Editor::TextPosition& u, const Editor::TextPosition& v) { return u < v; }; | |
| Editor::TextPosition start = std::min(pos1, pos2, cmp); | |
| Editor::TextPosition end = std::max(pos1, pos2, cmp); | |
| if (start == end || start.fParagraphIndex == fLines.size()) { | |
| return size; | |
| } | |
| if (start.fParagraphIndex == end.fParagraphIndex) { | |
| SkASSERT(end.fTextByteIndex > start.fTextByteIndex); | |
| auto& str = fLines[start.fParagraphIndex].fText; | |
| append(&dst, &size, str.begin() + start.fTextByteIndex, | |
| end.fTextByteIndex - start.fTextByteIndex); | |
| return size; | |
| } | |
| SkASSERT(end.fParagraphIndex < fLines.size()); | |
| const std::vector<TextLine>::const_iterator firstP = fLines.begin() + start.fParagraphIndex; | |
| const std::vector<TextLine>::const_iterator lastP = fLines.begin() + end.fParagraphIndex; | |
| const auto& first = firstP->fText; | |
| const auto& last = lastP->fText; | |
| append(&dst, &size, first.begin() + start.fTextByteIndex, first.size() - start.fTextByteIndex); | |
| for (auto line = firstP + 1; line < lastP; ++line) { | |
| append(&dst, &size, "\n", 1); | |
| append(&dst, &size, line->fText.begin(), line->fText.size()); | |
| } | |
| append(&dst, &size, "\n", 1); | |
| append(&dst, &size, last.begin(), end.fTextByteIndex); | |
| return size; | |
| } | |
| static inline const char* begin(const StringSlice& s) { return s.begin(); } | |
| static inline const char* end(const StringSlice& s) { return s.end(); } | |
| static size_t align_column(const StringSlice& 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 <typename T> | |
| static size_t find_first_larger(const std::vector<T>& 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<SkRect>& bounds, float x, size_t b, size_t e) { | |
| if (b >= e) { | |
| return b; | |
| } | |
| SkASSERT(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; | |
| } | |
| Editor::TextPosition Editor::move(Editor::Movement move, Editor::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); | |
| } | |
| SkASSERT(pos.fParagraphIndex < fLines.size()); | |
| SkASSERT(pos.fTextByteIndex <= fLines[pos.fParagraphIndex].fText.size()); | |
| SkASSERT(pos.fTextByteIndex == fLines[pos.fParagraphIndex].fText.size() || | |
| !is_utf8_continuation(fLines[pos.fParagraphIndex].fText.begin()[pos.fTextByteIndex])); | |
| switch (move) { | |
| case Editor::Movement::kNowhere: | |
| break; | |
| case Editor::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 Editor::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 Editor::Movement::kHome: | |
| { | |
| const std::vector<size_t>& list = fLines[pos.fParagraphIndex].fLineEndOffsets; | |
| size_t f = find_first_larger(list, pos.fTextByteIndex); | |
| pos.fTextByteIndex = f > 0 ? list[f - 1] : 0; | |
| } | |
| break; | |
| case Editor::Movement::kEnd: | |
| { | |
| const std::vector<size_t>& 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 Editor::Movement::kUp: | |
| { | |
| SkASSERT(pos.fTextByteIndex < fLines[pos.fParagraphIndex].fCursorPos.size()); | |
| float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left(); | |
| const std::vector<size_t>& 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 Editor::Movement::kDown: | |
| { | |
| const std::vector<size_t>& 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<size_t>& 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 Editor::Movement::kWordLeft: | |
| { | |
| if (pos.fTextByteIndex == 0) { | |
| pos = this->move(Editor::Movement::kLeft, pos); | |
| break; | |
| } | |
| const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries; | |
| SkASSERT(words.size() == fLines[pos.fParagraphIndex].fText.size()); | |
| do { | |
| --pos.fTextByteIndex; | |
| } while (pos.fTextByteIndex > 0 && !words[pos.fTextByteIndex]); | |
| } | |
| break; | |
| case Editor::Movement::kWordRight: | |
| { | |
| const StringSlice& text = fLines[pos.fParagraphIndex].fText; | |
| if (pos.fTextByteIndex == text.size()) { | |
| pos = this->move(Editor::Movement::kRight, pos); | |
| break; | |
| } | |
| const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries; | |
| SkASSERT(words.size() == text.size()); | |
| do { | |
| ++pos.fTextByteIndex; | |
| } while (pos.fTextByteIndex < text.size() && !words[pos.fTextByteIndex]); | |
| } | |
| break; | |
| } | |
| return pos; | |
| } | |
| void Editor::paint(SkCanvas* c, PaintOpts options) { | |
| this->reshapeAll(); | |
| if (!c) { | |
| return; | |
| } | |
| c->drawPaint(SkPaint(options.fBackgroundColor)); | |
| SkPaint selection = SkPaint(options.fSelectionColor); | |
| auto cmp = [](const Editor::TextPosition& u, const Editor::TextPosition& v) { return u < v; }; | |
| for (TextPosition pos = std::min(options.fSelectionBegin, options.fSelectionEnd, cmp), | |
| end = std::max(options.fSelectionBegin, options.fSelectionEnd, cmp); | |
| pos < end; | |
| pos = this->move(Editor::Movement::kRight, pos)) | |
| { | |
| SkASSERT(pos.fParagraphIndex < fLines.size()); | |
| const TextLine& l = fLines[pos.fParagraphIndex]; | |
| c->drawRect(offset(l.fCursorPos[pos.fTextByteIndex], l.fOrigin), selection); | |
| } | |
| if (fLines.size() > 0) { | |
| c->drawRect(Editor::getLocation(options.fCursor), SkPaint(options.fCursorColor)); | |
| } | |
| SkPaint foreground = SkPaint(options.fForegroundColor); | |
| for (const TextLine& line : fLines) { | |
| if (line.fBlob) { | |
| c->drawTextBlob(line.fBlob.get(), line.fOrigin.x(), line.fOrigin.y(), foreground); | |
| } | |
| } | |
| } | |
| void Editor::reshapeAll() { | |
| if (fNeedsReshape) { | |
| if (fLines.empty()) { | |
| fLines.push_back(TextLine()); | |
| } | |
| float shape_width = (float)(fWidth); | |
| #ifdef SK_EDITOR_GO_FAST | |
| SkSemaphore semaphore; | |
| std::unique_ptr<SkExecutor> executor = SkExecutor::MakeFIFOThreadPool(100); | |
| int jobCount = 0; | |
| for (TextLine& line : fLines) { | |
| if (!line.fShaped) { | |
| executor->add([&]() { | |
| ShapeResult result = Shape(line.fText.begin(), line.fText.size(), | |
| fFont, 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; | |
| semaphore.signal(); | |
| } | |
| ++jobCount; | |
| }); | |
| } | |
| while (jobCount-- > 0) { semaphore.wait(); } | |
| #else | |
| for (TextLine& line : fLines) { | |
| if (!line.fShaped) { | |
| ShapeResult result = Shape(line.fText.begin(), 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; | |
| } | |
| } | |
| #endif | |
| int y = 0; | |
| for (TextLine& line : fLines) { | |
| line.fOrigin = {0, y}; | |
| y += line.fHeight; | |
| } | |
| fHeight = y; | |
| fNeedsReshape = false; | |
| } | |
| } | |