8e9fc27822
Bug: skia:9020 Change-Id: I2f2ade4a5406b60e1b6c2004136fbbad36085d58 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/230121 Reviewed-by: Hal Canary <halcanary@google.com> Commit-Queue: Hal Canary <halcanary@google.com>
504 lines
18 KiB
C++
504 lines
18 KiB
C++
// 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<float[]> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool valid_utf8(const char* ptr, size_t size) { return SkUTF::CountUTF8(ptr, size) >= 0; }
|
|
|
|
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());
|
|
if (line->fText.size()) {
|
|
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.offset(line->fCursorPos[line->fText.size() - 1].fRight,
|
|
runHandler.yOffset()); // FIXME offset down.
|
|
}
|
|
float h = std::max(runHandler.endPoint().y(), font.getSpacing());
|
|
line->fHeight = (int)ceilf(h);
|
|
line->fBlob = runHandler.makeBlob();
|
|
line->fShaped = true;
|
|
}
|
|
|
|
// 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 const 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::setText(const char* data, size_t length) {
|
|
std::vector<Editor::TextLine> lines;
|
|
if (data && length && valid_utf8(data, length)) {
|
|
readlines(data, length, [&lines](const char* p, size_t s) {
|
|
lines.push_back(remove_newline(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(StringSlice(kSpace, strlen(kSpace)));
|
|
Editor::Shape(&textLine, shaper.get(), FLT_MAX, fFont, SkRect{0, 0, 0, 0});
|
|
fSpaceBounds = textLine.fCursorPos[0];
|
|
this->markAllDirty();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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)) {
|
|
return pos;
|
|
}
|
|
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;
|
|
fLines[pos.fParagraphIndex].fShaped = false;
|
|
} 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);
|
|
});
|
|
}
|
|
fNeedsReshape = true;
|
|
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);
|
|
fLines[start.fParagraphIndex].fBlob = nullptr;
|
|
fLines[start.fParagraphIndex].fShaped = false;
|
|
} 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);
|
|
line.fBlob = nullptr;
|
|
line.fShaped = false;
|
|
fLines.erase(fLines.begin() + start.fParagraphIndex + 1,
|
|
fLines.begin() + end.fParagraphIndex + 1);
|
|
}
|
|
return start;
|
|
}
|
|
|
|
StringSlice Editor::copy(TextPosition pos1, TextPosition pos2) const {
|
|
StringSlice result;
|
|
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 result;
|
|
}
|
|
if (start.fParagraphIndex == end.fParagraphIndex) {
|
|
SkASSERT(end.fTextByteIndex > start.fTextByteIndex);
|
|
auto& str = fLines[start.fParagraphIndex].fText;
|
|
result.insert(0, str.begin() + start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
|
|
} else {
|
|
SkASSERT(end.fParagraphIndex < fLines.size());
|
|
auto& str = fLines[start.fParagraphIndex].fText;
|
|
result.insert(0, str.begin() + start.fTextByteIndex, str.size() - start.fTextByteIndex);
|
|
for (const TextLine* line = &fLines.begin()[start.fParagraphIndex + 1];
|
|
line < &fLines.begin()[end.fParagraphIndex];
|
|
++line) {
|
|
result.insert(result.size(), "\n", 1);
|
|
result.insert(result.size(), line->fText.begin(), line->fText.size());
|
|
}
|
|
result.insert(result.size(), "\n", 1);
|
|
const auto& last = fLines.begin()[end.fParagraphIndex].fText;
|
|
result.insert(result.size(), last.begin(), end.fTextByteIndex);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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) const {
|
|
// First thing: fix possible bad values.
|
|
if (pos.fParagraphIndex >= fLines.size()) {
|
|
pos.fParagraphIndex = fLines.size();
|
|
pos.fTextByteIndex = 0;
|
|
} else {
|
|
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 (pos.fParagraphIndex == fLines.size()) {
|
|
break;
|
|
}
|
|
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;
|
|
line.fShaped = false;
|
|
}
|
|
fNeedsReshape = true;
|
|
};
|
|
|
|
void Editor::reshapeAll() {
|
|
if (fNeedsReshape) {
|
|
float shape_width = (float)(fWidth - 2 * fMargin);
|
|
#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([&]() {
|
|
Editor::Shape(&line, SkShaper::Make().get(), shape_width, fFont, fSpaceBounds);
|
|
semaphore.signal();
|
|
}
|
|
++jobCount;
|
|
});
|
|
}
|
|
while (jobCount-- > 0) { semaphore.wait(); }
|
|
#else
|
|
auto shaper = SkShaper::Make();
|
|
int i = 0;
|
|
for (TextLine& line : fLines) {
|
|
if (!line.fShaped) {
|
|
#ifdef SK_EDITOR_DEBUG_OUT
|
|
SkDebugf("shape %d: '%.*s'\n", i, line.fText.size(), line.fText.begin());
|
|
#endif // SK_EDITOR_DEBUG_OUT
|
|
Editor::Shape(&line, shaper.get(), shape_width, fFont, fSpaceBounds);
|
|
}
|
|
++i;
|
|
}
|
|
#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();
|
|
}
|
|
}
|