// 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 "experimental/editor/editor.h" #include "include/core/SkCanvas.h" #include "include/core/SkExecutor.h" #include "include/core/SkFontMetrics.h" #include "include/core/SkPath.h" #include "modules/skshaper/include/SkShaper.h" #include "src/utils/SkUTF.h" #include "experimental/editor/run_handler.h" using namespace editor; static constexpr SkRect kUnsetRect{-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX}; static SkRect selection_box(const SkFontMetrics& metrics, float advance, SkPoint pos) { if (fabsf(advance) < 1.0f) { advance = copysignf(1.0f, advance); } return SkRect{pos.x(), pos.y() + metrics.fAscent, pos.x() + advance, pos.y() + metrics.fDescent}.makeSorted(); } void callback_fn(void* context, const char* utf8Text, size_t utf8TextBytes, size_t glyphCount, const SkGlyphID* glyphs, const SkPoint* positions, const uint32_t* clusters, const SkFont& font) { SkASSERT(context); SkASSERT(glyphCount > 0); SkRect* cursors = (SkRect*)context; SkFontMetrics metrics; font.getMetrics(&metrics); std::unique_ptr advances(new float[glyphCount]); font.getWidths(glyphs, glyphCount, advances.get()); // Loop over each cluster in this run. size_t clusterStart = 0; for (size_t glyphIndex = 0; glyphIndex < glyphCount; ++glyphIndex) { if (glyphIndex + 1 < glyphCount // more glyphs && clusters[glyphIndex] == clusters[glyphIndex + 1]) { continue; // multi-glyph cluster } unsigned textBegin = clusters[glyphIndex]; unsigned textEnd = utf8TextBytes; for (size_t i = 0; i < glyphCount; ++i) { if (clusters[i] >= textEnd) { textEnd = clusters[i] + 1; } } for (size_t i = 0; i < glyphCount; ++i) { if (clusters[i] > textBegin && clusters[i] < textEnd) { textEnd = clusters[i]; if (textEnd == textBegin + 1) { break; } } } SkASSERT(glyphIndex + 1 > clusterStart); unsigned clusterGlyphCount = glyphIndex + 1 - clusterStart; const SkPoint* clusterGlyphPositions = &positions[clusterStart]; const float* clusterAdvances = &advances[clusterStart]; clusterStart = glyphIndex + 1; // for next loop SkRect clusterBox = selection_box(metrics, clusterAdvances[0], clusterGlyphPositions[0]); for (unsigned i = 1; i < clusterGlyphCount; ++i) { // multiple glyphs clusterBox.join(selection_box(metrics, clusterAdvances[i], clusterGlyphPositions[i])); } if (textBegin + 1 == textEnd) { // single byte, fast path. cursors[textBegin] = clusterBox; continue; } int textCount = textEnd - textBegin; int codePointCount = SkUTF::CountUTF8(utf8Text + textBegin, textCount); if (codePointCount == 1) { // single codepoint, fast path. cursors[textBegin] = clusterBox; continue; } float width = clusterBox.width() / codePointCount; SkASSERT(width > 0); const char* ptr = utf8Text + textBegin; const char* end = utf8Text + textEnd; float x = clusterBox.left(); while (ptr < end) { // for each codepoint in cluster const char* nextPtr = ptr; SkUTF::NextUTF8(&nextPtr, end); int firstIndex = ptr - utf8Text; float nextX = x + width; cursors[firstIndex] = SkRect{x, clusterBox.top(), nextX, clusterBox.bottom()}; x = nextX; ptr = nextPtr; } } } void Editor::Shape(TextLine* line, SkShaper* shaper, float width, const SkFont& font, SkRect space) { SkASSERT(line); SkASSERT(shaper); line->fCursorPos.resize(line->fText.size() + 1); for (SkRect& c : line->fCursorPos) { c = kUnsetRect; } RunHandler runHandler(line->fText.begin(), line->fText.size()); runHandler.setRunCallback(callback_fn, line->fCursorPos.data()); shaper->shape(line->fText.begin(), line->fText.size(), font, true, width, &runHandler); SkRect& last = line->fCursorPos[line->fText.size()]; last = space; if (line->fText.size() > 0) { last.fLeft = line->fCursorPos[line->fText.size() - 1].fRight; last.fRight = last.fLeft + space.width(); } float h = std::max(runHandler.endPoint().y(), font.getSpacing()); line->fHeight = (int)ceilf(h); line->fBlob = runHandler.makeBlob(); } // 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; f(start, len); start = ptr; } } void Editor::setText(const char* data, size_t length) { std::vector lines; if (data && length) { readlines(data, length, [&lines](const char* p, size_t s) { if (s > 0 && p[s - 1] == '\n') { --s; } // rstrip() lines.push_back(Editor::TextLine(p, s)); }); } fLines = std::move(lines); this->markAllDirty(); } void Editor::setFont(SkFont font) { if (font != fFont) { fFont = font; auto shaper = SkShaper::Make(); const char kSpace[] = " "; TextLine textLine(kSpace, strlen(kSpace)); Editor::Shape(&textLine, shaper.get(), FLT_MAX, fFont, SkRect{0, 0, 0, 0}); fSpaceBounds = textLine.fCursorPos[0]; this->markAllDirty(); } } Editor::TextPosition Editor::getPosition(SkIPoint xy) { this->reshapeAll(); for (size_t j = 0; j < fLines.size(); ++j) { const TextLine& line = fLines[j]; if (!line.fBlob) { continue; } SkIRect b = line.fBlob->bounds().roundOut().makeOffset(line.fOrigin.x(), line.fOrigin.y()); if (b.contains(xy.x(), xy.y())) { xy -= line.fOrigin; const std::vector& pos = line.fCursorPos; for (size_t i = 0; i < pos.size(); ++i) { if (pos[i] == kUnsetRect) { continue; } if (pos[i].contains((float)xy.x(), (float)xy.y())) { return Editor::TextPosition{i, j}; } } } } return Editor::TextPosition(); } 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; } Editor::TextPosition Editor::insert(TextPosition pos, const char* utf8Text, size_t byteLen) { //FIXME if (!valid_utf8(utf8Text, byteLen)) { return; } pos = this->move(Editor::Movement::kNowhere, pos); if (pos.fParagraphIndex < fLines.size()) { fLines[pos.fParagraphIndex].fText.insert(pos.fTextByteIndex, utf8Text, byteLen); fLines[pos.fParagraphIndex].fBlob = nullptr; fNeedsReshape = true; return Editor::TextPosition{pos.fTextByteIndex + byteLen, pos.fParagraphIndex}; } else { // append new line } 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; } if (start.fParagraphIndex == end.fParagraphIndex) { SkASSERT(end.fTextByteIndex > start.fTextByteIndex); fLines[start.fParagraphIndex].fText.remove( start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex); fLines[start.fParagraphIndex].fBlob = nullptr; fNeedsReshape = true; } else { // delete across lines } return start; } 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); } Editor::TextPosition Editor::move(Editor::Movement move, Editor::TextPosition pos) { pos.fParagraphIndex = std::min(pos.fParagraphIndex, fLines.size()); pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, 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: pos.fTextByteIndex = 0; break; case Editor::Movement::kEnd: pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size(); break; case Editor::Movement::kUp: if (pos.fParagraphIndex > 0) { --pos.fParagraphIndex; pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex); } break; case Editor::Movement::kDown: if (pos.fParagraphIndex + 1 < fLines.size()) { ++pos.fParagraphIndex; pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex); } break; } return pos; } static inline SkRect offset(SkRect r, SkIPoint p) { r.offset((float)p.x(), (float)p.y()); return r; } 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) { const TextLine& cLine = fLines[options.fCursor.fParagraphIndex]; SkRect pos = fSpaceBounds; if (options.fCursor.fTextByteIndex < cLine.fCursorPos.size()) { pos = cLine.fCursorPos[options.fCursor.fTextByteIndex]; } pos.fRight = pos.fLeft + 1; pos.fLeft -= 1; c->drawRect(offset(pos, cLine.fOrigin), 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::markAllDirty() { for (TextLine& line : fLines) { line.fBlob = nullptr; } fNeedsReshape = true; }; void Editor::reshapeAll() { if (fNeedsReshape) { float shape_width = (float)(fWidth - 2 * fMargin); #ifdef SK_EDITOR_GO_FAST SkSemaphore semaphore; std::unique_ptr executor = SkExecutor::MakeFIFOThreadPool(100); int jobCount = 0; for (TextLine& line : fLines) { if (!line.textBlob()) { executor->add([&]() { Editor::Shape(&line, SkShaper::Make().get(), shape_width, fFont, fSpaceBounds); semaphore.signal(); } ++jobCount; }); } while (jobCount-- > 0) { semaphore.wait(); } #else auto shaper = SkShaper::Make(); for (TextLine& line : fLines) { if (!line.fBlob) { Editor::Shape(&line, shaper.get(), shape_width, fFont, fSpaceBounds); } } #endif int y = fMargin; for (TextLine& line : fLines) { line.fOrigin = {fMargin, y}; y += line.fHeight; } fHeight = y + fMargin; fNeedsReshape = false; } } void Editor::setWidth(int w) { if (fWidth != w) { fWidth = w; this->markAllDirty(); } }