Initial Commit

This commit is contained in:
Yousef Khadadeh 2025-07-06 20:43:53 +03:00
commit 6088fc29ed
978 changed files with 353647 additions and 0 deletions

View file

@ -0,0 +1,498 @@
// 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/core/SkCanvas.h>
#include <include/core/SkPath.h>
#include <src/base/SkUTF.h>
#include <algorithm>
#include <cfloat>
#include <cassert>
#include <utility>
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 <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;
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<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) {
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<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 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<TextParagraph>::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<TextParagraph>::const_iterator firstP = fLines.begin() + start.fParagraphIndex;
const std::vector<TextParagraph>::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 <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;
}
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<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 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 Movement::kUp:
{
assert(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 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 Movement::kWordLeft:
{
if (pos.fTextByteIndex == 0) {
pos = this->move(Movement::kLeft, pos);
break;
}
const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries;
assert(words.size() == fLines[pos.fParagraphIndex].fText.size());
do {
--pos.fTextByteIndex;
} while (pos.fTextByteIndex > 0 && !words[pos.fTextByteIndex]);
}
break;
case Movement::kWordRight:
{
const std::string& text = fLines[pos.fParagraphIndex].fText;
if (pos.fTextByteIndex == text.size()) {
pos = this->move(Movement::kRight, pos);
break;
}
const std::vector<bool>& words = fLines[pos.fParagraphIndex].fWordBoundaries;
assert(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));
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;
}
}

View file

@ -0,0 +1,125 @@
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
#pragma once
#include "CollabTextBoxCursor.hpp"
#include <include/core/SkColor.h>
#include <include/core/SkFont.h>
#include <include/core/SkFontMgr.h>
#include <include/core/SkRefCnt.h>
#include <include/core/SkTextBlob.h>
#include <vector>
#include <string_view>
#include <string>
#include <Eigen/Dense>
class SkCanvas;
class SkShaper;
using namespace Eigen;
namespace CollabTextBox {
struct TextParagraph {
TextParagraph(std::string_view t) : fText(t) {}
TextParagraph() {}
std::string fText;
sk_sp<const SkTextBlob> fBlob;
std::vector<SkRect> fCursorPos;
std::vector<size_t> fLineEndOffsets;
std::vector<bool> fWordBoundaries;
SkIPoint fOrigin = {0, 0};
int fHeight = 0;
bool fShaped = false;
};
enum class Movement {
kNowhere,
kLeft,
kUp,
kRight,
kDown,
kHome,
kEnd,
kWordLeft,
kWordRight,
};
bool is_utf8_continuation(char v);
const char* next_utf8(const char* p, const char* end);
const char* align_utf8(const char* p, const char* begin);
const char* prev_utf8(const char* p, const char* begin);
class Editor {
public:
// total height in canvas display units.
int getHeight() const { return fHeight; }
// set display width in canvas display units
void setWidth(int w); // may force re-shape
// get/set current font (used for shaping and displaying text)
const SkFont& font() const { return fFont; }
void setFont(SkFont font);
void setFontMgr(sk_sp<SkFontMgr> fontMgr);
struct Text {
const std::vector<TextParagraph>& fLines;
struct Iterator {
std::vector<TextParagraph>::const_iterator fPtr;
std::string_view operator*() { return std::string_view(fPtr->fText); }
void operator++() { ++fPtr; }
bool operator!=(const Iterator& other) const { return fPtr != other.fPtr; }
};
Iterator begin() const { return Iterator{fLines.begin()}; }
Iterator end() const { return Iterator{fLines.end()}; }
};
// Loop over all the lines of text. The lines are not '\0'- or '\n'-terminated.
// For example, to dump the entire file to standard output:
// for (SkPlainTextEditor::StringView str : editor.text()) {
// std::cout.write(str.data, str.size) << '\n';
// }
Text text() const { return Text{fLines}; }
std::string get_string() const;
// get size of line in canvas display units.
int lineHeight(size_t index) const { return fLines[index].fHeight; }
TextPosition move(Movement move, TextPosition pos) const;
TextPosition getPosition(SkIPoint);
SkRect getLocation(TextPosition);
// insert into current text.
TextPosition insert(TextPosition, std::string_view utf8Text);
// remove text between two positions
TextPosition remove(TextPosition, TextPosition);
std::string copy(TextPosition pos1, TextPosition pos2) const;
size_t lineCount() const { return fLines.size(); }
std::string_view line(size_t i) const {
return i < fLines.size() ? std::string_view(fLines[i].fText) : std::string_view();
}
struct PaintOpts {
SkColor4f fBackgroundColor = {1, 1, 1, 1};
SkColor4f fForegroundColor = {0, 0, 0, 1};
SkColor4f cursorColor = {1, 0, 0, 1};
bool showCursor = false;
Cursor cursor;
};
void paint(SkCanvas* canvas, PaintOpts options);
std::vector<TextParagraph> fLines;
private:
int fWidth = 0;
int fHeight = 0;
SkFont fFont;
sk_sp<SkFontMgr> fFontMgr;
bool fNeedsReshape = false;
const char* fLocale = "en"; // TODO: make this setable
void markDirty(TextParagraph*);
void reshapeAll();
};
}

View file

@ -0,0 +1,33 @@
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
#pragma once
#include <iostream>
#include <limits>
namespace CollabTextBox {
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<uint32_t>::max();
size_t fParagraphIndex = std::numeric_limits<uint32_t>::max();
template <typename Archive> void serialize(Archive& a) {
a((uint32_t)fTextByteIndex, (uint32_t)fParagraphIndex);
}
bool operator==(const TextPosition& o) const = default;
};
static inline bool operator<(const CollabTextBox::TextPosition& u,
const CollabTextBox::TextPosition& v) {
return u.fParagraphIndex < v.fParagraphIndex ||
(u.fParagraphIndex == v.fParagraphIndex && u.fTextByteIndex < v.fTextByteIndex);
}
struct Cursor {
TextPosition pos{0, 0};
TextPosition selectionBeginPos{0, 0};
TextPosition selectionEndPos{0, 0};
template <typename Archive> void serialize(Archive& a) {
a(pos, selectionBeginPos, selectionEndPos);
}
bool operator==(const Cursor& o) const = default;
};
}

View file

@ -0,0 +1,365 @@
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
#include "CollabTextBoxShaper.hpp"
#include <include/core/SkFont.h>
#include <include/core/SkFontMetrics.h>
#include <include/core/SkFontMgr.h>
#include <include/core/SkPoint.h>
#include <include/core/SkRefCnt.h>
#include <include/core/SkScalar.h>
#include <include/core/SkString.h>
#include <include/core/SkTextBlob.h>
#include <include/core/SkTypes.h>
#include <include/private/base/SkTFitsIn.h>
#include <modules/skshaper/include/SkShaper.h>
#include <src/base/SkUTF.h>
#include <modules/skshaper/include/SkShaper_harfbuzz.h>
#include <modules/skshaper/include/SkShaper_skunicode.h>
#include <modules/skshaper/include/SkShaper_factory.h>
#include <modules/skunicode/include/SkUnicode.h>
#include <modules/skunicode/include/SkUnicode_icu.h>
#include <cfloat>
#include <climits>
#include <cstring>
sk_sp<SkUnicode> get_unicode_2() {
if(auto unicode = SkUnicodes::ICU::Make()) {
return unicode;
}
SkDEBUGFAIL("Cannot make SkUnicode");
return nullptr;
}
namespace {
class RunHandler final : public SkShaper::RunHandler {
public:
RunHandler(const char* utf8Text, size_t) : fUtf8Text(utf8Text) {}
using RunCallback = void (*)(void* context,
const char* utf8Text,
size_t utf8TextBytes,
size_t glyphCount,
const SkGlyphID* glyphs,
const SkPoint* positions,
const uint32_t* clusters,
const SkFont& font);
void setRunCallback(RunCallback f, void* context) {
fCallbackContext = context;
fCallbackFunction = f;
}
sk_sp<SkTextBlob> makeBlob();
SkPoint endPoint() const { return fOffset; }
SkPoint finalPosition() const { return fCurrentPosition; }
void beginLine() override;
void runInfo(const RunInfo&) override;
void commitRunInfo() override;
SkShaper::RunHandler::Buffer runBuffer(const RunInfo&) override;
void commitRunBuffer(const RunInfo&) override;
void commitLine() override;
const std::vector<size_t>& lineEndOffsets() const { return fLineEndOffsets; }
SkRect finalRect(const SkFont& font) const {
if (0 == fMaxRunAscent || 0 == fMaxRunDescent) {
SkFontMetrics metrics;
font.getMetrics(&metrics);
return {fCurrentPosition.x(),
fCurrentPosition.y(),
fCurrentPosition.x() + font.getSize(),
fCurrentPosition.y() + metrics.fDescent - metrics.fAscent};
} else {
return {fCurrentPosition.x(),
fCurrentPosition.y() + fMaxRunAscent,
fCurrentPosition.x() + font.getSize(),
fCurrentPosition.y() + fMaxRunDescent};
}
}
private:
SkTextBlobBuilder fBuilder;
std::vector<size_t> fLineEndOffsets;
const SkGlyphID* fCurrentGlyphs = nullptr;
const SkPoint* fCurrentPoints = nullptr;
void* fCallbackContext = nullptr;
RunCallback fCallbackFunction = nullptr;
char const * const fUtf8Text;
size_t fTextOffset = 0;
uint32_t* fClusters = nullptr;
int fClusterOffset = 0;
int fGlyphCount = 0;
SkScalar fMaxRunAscent = 0;
SkScalar fMaxRunDescent = 0;
SkScalar fMaxRunLeading = 0;
SkPoint fCurrentPosition = {0, 0};
SkPoint fOffset = {0, 0};
};
} // namespace
void RunHandler::beginLine() {
fCurrentPosition = fOffset;
fMaxRunAscent = 0;
fMaxRunDescent = 0;
fMaxRunLeading = 0;
}
void RunHandler::runInfo(const SkShaper::RunHandler::RunInfo& info) {
SkFontMetrics metrics;
info.fFont.getMetrics(&metrics);
fMaxRunAscent = std::min(fMaxRunAscent, metrics.fAscent);
fMaxRunDescent = std::max(fMaxRunDescent, metrics.fDescent);
fMaxRunLeading = std::max(fMaxRunLeading, metrics.fLeading);
}
void RunHandler::commitRunInfo() {
fCurrentPosition.fY -= fMaxRunAscent;
}
SkShaper::RunHandler::Buffer RunHandler::runBuffer(const RunInfo& info) {
int glyphCount = SkTFitsIn<int>(info.glyphCount) ? info.glyphCount : INT_MAX;
int utf8RangeSize = SkTFitsIn<int>(info.utf8Range.size()) ? info.utf8Range.size() : INT_MAX;
const auto& runBuffer = fBuilder.allocRunTextPos(info.fFont, glyphCount, utf8RangeSize);
fCurrentGlyphs = runBuffer.glyphs;
fCurrentPoints = runBuffer.points();
if (runBuffer.utf8text && fUtf8Text) {
memcpy(runBuffer.utf8text, fUtf8Text + info.utf8Range.begin(), utf8RangeSize);
}
fClusters = runBuffer.clusters;
fGlyphCount = glyphCount;
fClusterOffset = info.utf8Range.begin();
return {runBuffer.glyphs,
runBuffer.points(),
nullptr,
runBuffer.clusters,
fCurrentPosition};
}
void RunHandler::commitRunBuffer(const RunInfo& info) {
// for (size_t i = 0; i < info.glyphCount; ++i) {
// SkASSERT(fClusters[i] >= info.utf8Range.begin());
// // this fails for khmer example.
// SkASSERT(fClusters[i] < info.utf8Range.end());
// }
if (fCallbackFunction) {
fCallbackFunction(fCallbackContext,
fUtf8Text,
info.utf8Range.end(),
info.glyphCount,
fCurrentGlyphs,
fCurrentPoints,
fClusters,
info.fFont);
}
SkASSERT(0 <= fClusterOffset);
for (int i = 0; i < fGlyphCount; ++i) {
SkASSERT(fClusters[i] >= (unsigned)fClusterOffset);
fClusters[i] -= fClusterOffset;
}
fCurrentPosition += info.fAdvance;
fTextOffset = std::max(fTextOffset, info.utf8Range.end());
}
void RunHandler::commitLine() {
if (fLineEndOffsets.empty() || fTextOffset > fLineEndOffsets.back()) {
// Ensure that fLineEndOffsets is monotonic.
fLineEndOffsets.push_back(fTextOffset);
}
fOffset += { 0, fMaxRunDescent + fMaxRunLeading - fMaxRunAscent };
}
sk_sp<SkTextBlob> RunHandler::makeBlob() {
return fBuilder.make();
}
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();
}
static void set_character_bounds(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;
}
}
}
CollabTextBox::ShapeResult CollabTextBox::shape(const char* utf8Text,
size_t textByteLen,
const SkFont& font,
sk_sp<SkFontMgr> fontMgr,
const char* locale,
float width)
{
ShapeResult result;
if (SkUTF::CountUTF8(utf8Text, textByteLen) < 0) {
utf8Text = nullptr;
textByteLen = 0;
}
std::unique_ptr<SkShaper> shaper = nullptr;
auto unicode = get_unicode_2();
// USING PRIMITIVE WRAPPER INSTEAD OF HARFBUZZ, BECAUSE HARFBUZZ HAS ISSUES ON EMSCRIPTEN
// WE MIGHT HAVE TO CHANGE THIS BACK LATER WHEN THERE'S A FIX
//shaper = SkShapers::HB::ShaperDrivenWrapper(unicode, fontMgr);
shaper = SkShaper::MakePrimitive();
SkASSERT(shaper);
float height = font.getSpacing();
RunHandler runHandler(utf8Text, textByteLen);
if (textByteLen) {
result.glyphBounds.resize(textByteLen);
for (SkRect& c : result.glyphBounds) {
c = SkRect{-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX};
}
runHandler.setRunCallback(set_character_bounds, result.glyphBounds.data());
static constexpr uint8_t kBidiLevelLTR = 0;
std::unique_ptr<SkShaper::BiDiRunIterator> bidi = nullptr;
bidi = SkShapers::unicode::BidiRunIterator(
unicode, utf8Text, textByteLen, kBidiLevelLTR);
//if (!bidi) {
// throw std::runtime_error("[CollabTextBox] SkShaper unicode or harfbuzz not available");
// bidi = std::make_unique<SkShaper::TrivialBiDiRunIterator>(kBidiLevelLTR, textByteLen);
//}
SkASSERT(bidi);
std::unique_ptr<SkShaper::LanguageRunIterator> language =
SkShaper::MakeStdLanguageRunIterator(utf8Text, textByteLen);
SkASSERT(language);
std::unique_ptr<SkShaper::ScriptRunIterator> script =
SkShaper::MakeScriptRunIterator(utf8Text, textByteLen, SkSetFourByteTag('Z','z','z','z'));
SkASSERT(script);
std::unique_ptr<SkShaper::FontRunIterator> fontRuns =
SkShaper::MakeFontMgrRunIterator(utf8Text, textByteLen, font, fontMgr);
SkASSERT(fontRuns);
shaper->shape(utf8Text,
textByteLen,
*fontRuns,
*bidi,
*script,
*language,
nullptr,
0,
width,
&runHandler);
if (runHandler.lineEndOffsets().size() > 1) {
result.lineBreakOffsets = runHandler.lineEndOffsets();
SkASSERT(result.lineBreakOffsets.size() > 0);
result.lineBreakOffsets.pop_back();
}
height = std::max(height, runHandler.endPoint().y());
result.blob = runHandler.makeBlob();
}
result.glyphBounds.push_back(runHandler.finalRect(font));
result.verticalAdvance = (int)ceilf(height);
result.wordBreaks = get_utf8_word_boundaries(utf8Text, textByteLen, locale);
return result;
}
std::vector<bool> CollabTextBox::get_utf8_word_boundaries(const char* begin, std::size_t byteCount, const char* locale) {
auto unicode = get_unicode_2();
if (nullptr == unicode) {
return {};
}
std::vector<SkUnicode::Position> positions;
if (!unicode->getWords(begin, byteCount, locale, &positions) || byteCount == 0) {
return {};
}
std::vector<bool> result;
result.resize(byteCount);
for (auto& pos : positions) {
if(pos < result.size()) // Sometimes the position to cut at is out of bounds
result[pos] = true;
}
return result;
}

View file

@ -0,0 +1,21 @@
// This file has been copied from the Skia library, and may be slightly modified. Please check the NOTICE file for more details
#pragma once
#include <include/core/SkFont.h>
#include <include/core/SkTextBlob.h>
#include <cstddef>
#include <vector>
namespace CollabTextBox {
struct ShapeResult {
sk_sp<SkTextBlob> blob;
std::vector<size_t> lineBreakOffsets;
std::vector<SkRect> glyphBounds;
std::vector<bool> wordBreaks;
int verticalAdvance;
};
ShapeResult shape(const char* ut8text, size_t textByteLen, const SkFont& font, sk_sp<SkFontMgr> fontMgr, const char* locale, float width);
std::vector<bool> get_utf8_word_boundaries(const char* begin, size_t byteCount, const char* locale);
};