The first draft of improved & simplified Text API

Change-Id: If48f8ace53bcf3d2ac6c3c3ad87658f3901872eb
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/375134
Reviewed-by: Julia Lavrova <jlavrova@google.com>
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2021-02-24 12:15:08 -05:00 committed by Skia Commit-Bot
parent 5276ba274b
commit 159fd844c3
19 changed files with 1617 additions and 0 deletions

View File

@ -1530,6 +1530,10 @@ group("modules") {
]
}
group("experimental") {
deps = [ "experimental/sktext" ]
}
config("our_vulkan_headers") {
include_dirs = [ "include/third_party/vulkan" ]
}
@ -2081,6 +2085,7 @@ if (skia_enable_tools) {
":flags",
":gpu_tool_utils",
":xml",
"experimental/sktext:samples",
"modules/audioplayer",
"modules/skparagraph:samples",
"modules/skshaper",
@ -2754,6 +2759,7 @@ if (skia_enable_tools) {
":tool_utils",
":trace",
"experimental/skrive",
"experimental/sktext",
"modules/audioplayer",
"modules/particles",
"modules/skottie",

View File

@ -0,0 +1,96 @@
# Copyright 2021 Google LLC.
import("../../gn/skia.gni")
import("../../modules/skshaper/skshaper.gni")
declare_args() {
skia_enable_sktext = true
text_gms_enabled = true
text_tests_enabled = true
text_bench_enabled = false
}
if (skia_enable_sktext && skia_enable_skshaper && skia_use_icu &&
skia_use_harfbuzz) {
config("public_config") {
include_dirs = [ "include" ]
}
component("sktext") {
import("sktext.gni")
public_configs = [ ":public_config" ]
public = sktext_public
sources = sktext_sources
deps = [
"../..:skia",
"../../modules/skshaper",
]
}
if (defined(is_skia_standalone) && skia_enable_tools) {
source_set("gm") {
if (text_gms_enabled) {
testonly = true
sources = []
deps = [
":sktext",
"../..:gpu_tool_utils",
"../..:skia",
"../../modules/skshaper",
]
} else {
sources = []
}
}
source_set("tests") {
if (text_tests_enabled) {
testonly = true
sources = []
deps = [
":sktext",
"../..:gpu_tool_utils",
"../..:skia",
"../../modules/skshaper",
]
} else {
sources = []
}
}
source_set("bench") {
if (text_bench_enabled) {
testonly = true
sources = []
deps = [
":sktext",
"../..:skia",
"../../modules/skshaper",
]
} else {
sources = []
}
}
source_set("samples") {
testonly = true
sources = [ "samples/Text.cpp" ]
deps = [
":sktext",
"../..:skia",
"../../modules/skshaper",
]
}
}
} else {
group("sktext") {
}
group("gm") {
}
group("tests") {
}
group("bench") {
}
group("samples") {
}
}

View File

@ -0,0 +1,200 @@
// Copyright 2021 Google LLC.
#ifndef Processor_DEFINED
#define Processor_DEFINED
#include <string>
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/TextRun.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkPaint.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkTextBlob.h"
#include "modules/skshaper/src/SkUnicode.h"
namespace skia {
namespace text {
class Block {
public:
enum BlockType {
kFont,
kDecor,
kFormat,
};
Block(BlockType type, Range range)
: fType(type)
, fRange(range) { }
BlockType fType;
Range fRange;
};
class FontBlock : public Block {
public:
FontBlock(const SkString& family, SkScalar size, SkFontStyle style, Range range)
: Block(Block::kFont, range)
, fFontFamily(family)
, fFontSize(size)
, fFontStyle(style) { }
SkString fFontFamily;
SkScalar fFontSize;
SkFontStyle fFontStyle;
// TODO: Features
};
class DecorBlock : public Block {
public:
DecorBlock(const SkPaint* foreground, const SkPaint* background, Range range)
: Block(Block::kDecor, range)
, fForegroundColor(foreground)
, fBackgroundColor(background) { }
DecorBlock(Range range)
: DecorBlock(nullptr, nullptr, range) { }
const SkPaint* fForegroundColor;
const SkPaint* fBackgroundColor;
// Everything else
};
class TextFormatStyle {
public:
TextFormatStyle(TextAlign textAlign, TextDirection defaultTextDirection)
: fTextAlign(textAlign), fDefaultTextDirection(defaultTextDirection) { }
TextAlign fTextAlign;
TextDirection fDefaultTextDirection;
};
class TextFontStyle {
public:
TextFontStyle(TextDirection textDirection, sk_sp<SkFontMgr> fontManager)
: fTextDirection(textDirection), fFontManager(fontManager) { }
TextDirection fTextDirection;
sk_sp<SkFontMgr> fFontManager;
};
class TextOutput {
public:
TextOutput(sk_sp<SkTextBlob> textBlob, const SkPaint& paint, SkSize offset) : fTextBlob(std::move(textBlob)), fPaint(paint), fOffset(offset) { }
sk_sp<SkTextBlob> fTextBlob;
SkPaint fPaint;
SkSize fOffset;
};
class Processor {
public:
Processor(const SkString& text)
: fText(text)
, fUnicode(nullptr) {}
~Processor() = default;
// All the Unicode information
SkUnicode* getUnicode() { return fUnicode == nullptr ? nullptr : fUnicode.get(); }
// Once the text is measured you can get approximate sizing for it
bool shape(TextFontStyle fontStyle, SkTArray<FontBlock, true> fontBlocks);
// Once the text is fit into the required box you can get the real sizes for it
bool wrap(SkScalar width, SkScalar height);
// Once the text is formatted you can get it's glyphs info line by line
bool format(TextFormatStyle textFormatStyle);
// Once the text is decorated you can iterate it by segments (intersect of run, decor block and line)
bool decorate(SkTArray<DecorBlock, true> decorBlocks);
bool hasProperty(size_t index, CodeUnitFlags flag) {
return (fCodeUnitProperties[index] & flag) == flag;
}
bool isHardLineBreak(size_t index) {
return this->hasProperty(index, CodeUnitFlags::kHardLineBreakBefore);
}
bool isSoftLineBreak(size_t index) {
return this->hasProperty(index, CodeUnitFlags::kSoftLineBreakBefore);
}
bool isWhitespaces(Range range) {
if (range.leftToRight()) {
for (auto i = range.fStart; i < range.fEnd; ++i) {
if (!this->hasProperty(i, CodeUnitFlags::kPartOfWhiteSpace)) {
return false;
}
}
} else {
for (auto i = range.fStart; i > range.fEnd; --i) {
if (!this->hasProperty(i, CodeUnitFlags::kPartOfWhiteSpace)) {
return false;
}
}
}
return true;
}
bool isClusterEdge(size_t index) {
return this->hasProperty(index, CodeUnitFlags::kGraphemeStart) ||
this->hasProperty(index, CodeUnitFlags::kGlyphStart);
}
void adjustLeft(size_t* index) {
SkASSERT(index != nullptr);
while (*index != 0) {
if (isClusterEdge(*index)) {
return;
}
--index;
}
}
TextRun& run(const size_t index) { return fRuns[index]; }
// Simplification (using default font manager, default font family and default everything possible)
static bool drawText(const char* text, SkCanvas* canvas, SkScalar x, SkScalar y);
//static bool drawText(const char* text, SkCanvas* canvas, TextFormatStyle textFormat, const SkString& fontFamily, SkScalar fontSize, SkFontStyle fontStyle, SkScalar x, SkScalar y);
static bool drawText(const char* text, SkCanvas* canvas, TextFormatStyle textFormat, SkColor foreground, SkColor background, const SkString& fontFamily, SkScalar fontSize, SkFontStyle fontStyle, SkScalar x, SkScalar y);
void sortDecorBlocks(SkTArray<DecorBlock, true>& decorBlocks);
bool computeCodeUnitProperties();
void markGlyphs();
// Iterating through the input code units and breaking the runs by units flag (no breaking if units == CodeUnitFlags::kNonExistingFlag)
template<typename Visitor>
void iterateByLogicalOrder(CodeUnitFlags units, Visitor visitor);
// Iterating through the output glyphs and breaking the runs by units flag (no breaking if units == CodeUnitFlags::kNonExistingFlag)
template<typename Visitor>
void iterateByVisualOrder(CodeUnitFlags units, Visitor visitor);
template<typename Visitor>
void iterateByVisualOrder(SkTArray<DecorBlock, true>& decorBlocks, Visitor visitor);
private:
friend class TextIterator;
friend class Shaper;
friend class Wrapper;
SkString fText;
SkTArray<FontBlock, true> fFontBlocks;
//TextFormatStyle fTextFormatStyle;
//TextFontStyle fTextFontStyle;
SkTArray<TextRun, false> fRuns;
SkTArray<Line, false> fLines;
SkTArray<TextOutput, false> fTextOutputs;
std::unique_ptr<SkUnicode> fUnicode;
SkTArray<CodeUnitFlags, true> fCodeUnitProperties;
};
} // namespace text
} // namespace skia
#endif // Processor_DEFINED

View File

@ -0,0 +1,127 @@
// Copyright 2021 Google LLC.
#ifndef Types_DEFINED
#define Types_DEFINED
#include <algorithm>
#include <cstddef>
#include "include/private/SkBitmaskEnum.h"
#include "include/private/SkTo.h"
namespace skia {
namespace text {
enum class TextAlign {
kLeft,
kRight,
kCenter,
kJustify,
kStart,
kEnd,
};
enum class TextDirection {
kRtl,
kLtr,
};
enum class LineBreakType {
kSortLineBreakBefore,
kHardLineBreakBefore,
};
enum CodeUnitFlags : uint64_t {
kNoCodeUnitFlag = 0x0,
kPartOfWhiteSpace = 0x1,
kGraphemeStart = 0x2,
kSoftLineBreakBefore = 0x4,
kHardLineBreakBefore = 0x8,
// This information we get from SkShaper
kGlyphStart = 0x16,
kGlyphClusterStart = 0x32,
kNonExistingFlag = 0x64,
};
class Range {
public:
Range() : fStart(0), fEnd(0) { }
Range(size_t start, size_t end) : fStart(start) , fEnd(end) { }
bool leftToRight() const {
return fEnd >= fStart;
}
void normalize() {
if (!this->leftToRight()) {
std::swap(this->fStart, this->fEnd);
}
}
// For RTL ranges start >= end
int width() const {
return leftToRight() ? SkToInt(fEnd - fStart) : SkToInt(fStart - fEnd);
}
void clean() {
fStart = 0;
fEnd = 0;
}
bool isEmpty() const {
return fEnd == fStart;
}
void merge(Range tail) {
auto ltr = this->leftToRight();
this->normalize();
tail.normalize();
SkASSERT(this->fEnd == tail.fStart || this->fStart == tail.fEnd);
this->fStart = std::min(this->fStart, tail.fStart);
this->fEnd = std::max(this->fEnd, tail.fEnd);
if (!ltr) {
std::swap(this->fStart, this->fEnd);
}
}
void intersect(Range other) {
auto ltr = this->leftToRight();
this->normalize();
other.normalize();
this->fStart = std::max(this->fStart, other.fStart);
this->fEnd = std::min(this->fEnd, other.fEnd);
if (this->fStart > this->fEnd) {
// There is nothing in the intersection; make it empty
this->fEnd = this->fStart;
} else if (!ltr) {
std::swap(this->fStart, this->fEnd);
}
}
template<typename Visitor>
void iterate(Visitor visitor) {
if (this->leftToRight()) {
for (auto index = this->fStart; index < this->fEnd; ++index) {
visitor(index);
}
} else {
for (auto index = this->fStart; index < this->fEnd; --index) {
visitor(index);
}
}
}
size_t fStart;
size_t fEnd;
};
const size_t EMPTY_INDEX = std::numeric_limits<size_t>::max();
const Range EMPTY_RANGE = Range(EMPTY_INDEX, EMPTY_INDEX);
} // namespace text
} // namespace skia
namespace sknonstd {
template <> struct is_bitmask_enum<skia::text::CodeUnitFlags> : std::true_type {};
}
#endif

View File

@ -0,0 +1,122 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Processor.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColorFilter.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkGraphics.h"
#include "include/core/SkPath.h"
#include "include/core/SkRegion.h"
#include "include/core/SkShader.h"
#include "include/core/SkStream.h"
#include "include/core/SkTime.h"
#include "samplecode/Sample.h"
#include "src/core/SkOSFile.h"
#include "src/shaders/SkColorShader.h"
#include "src/utils/SkOSPath.h"
#include "src/utils/SkUTF.h"
#include "tools/Resources.h"
#include "tools/flags/CommandLineFlags.h"
using namespace skia::text;
namespace {
class TextSample_HelloWorld : public Sample {
protected:
SkString name() override { return SkString("TextSample_HelloWorld"); }
void onDrawContent(SkCanvas* canvas) override {
canvas->drawColor(SK_ColorWHITE);
Processor::drawText("Hello word", canvas, 0, 0);
}
private:
using INHERITED = Sample;
};
class TextSample_Align_Dir : public Sample {
public:
TextSample_Align_Dir() : fUnicode(SkUnicode::Make()) { }
protected:
SkString name() override { return SkString("TextSample_Align_Dir"); }
void drawLine(SkCanvas* canvas, SkScalar w, SkScalar h,
const std::string& text,
TextAlign align,
TextDirection direction = TextDirection::kLtr) {
SkColor background = SK_ColorGRAY;
const std::u16string& ellipsis = u"\u2026";
SkScalar margin = 20;
SkAutoCanvasRestore acr(canvas, true);
canvas->clipRect(SkRect::MakeWH(w, h));
canvas->drawColor(SK_ColorWHITE);
Processor::drawText(direction == TextDirection::kRtl ? mirror(text).c_str() : normal(text).c_str(),
canvas,
TextFormatStyle(align, direction),
SK_ColorBLACK, SK_ColorLTGRAY,
SkString("Roboto"), 12.0f, SkFontStyle::Normal(),
0, 0);
}
SkString mirror(const std::string& text) {
std::u16string result;
result += u"\u202E";
//for (auto i = text.size(); i > 0; --i) {
// result += text[i - 1];
//}
for (auto ch : text) {
result += ch;
}
result += u"\u202C";
return fUnicode->convertUtf16ToUtf8(result);
}
SkString normal(const std::string& text) {
std::u16string result;
result += u"\u202D";
for (auto ch : text) {
result += ch;
}
result += u"\u202C";
return fUnicode->convertUtf16ToUtf8(result);
}
void onDrawContent(SkCanvas* canvas) override {
canvas->drawColor(SK_ColorDKGRAY);
SkScalar width = this->width() / 4;
SkScalar height = this->height() / 2;
const std::string line = "One line of text";
drawLine(canvas, width, height, line, TextAlign::kLeft, TextDirection::kLtr);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kRight, TextDirection::kLtr);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kCenter, TextDirection::kLtr);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kJustify, TextDirection::kLtr);
canvas->translate(-width * 3, height);
drawLine(canvas, width, height, line, TextAlign::kLeft, TextDirection::kRtl);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kRight, TextDirection::kRtl);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kCenter, TextDirection::kRtl);
canvas->translate(width, 0);
drawLine(canvas, width, height, line, TextAlign::kJustify, TextDirection::kRtl);
canvas->translate(width, 0);
}
private:
using INHERITED = Sample;
std::unique_ptr<SkUnicode> fUnicode;
};
} // namespace
DEF_SAMPLE(return new TextSample_HelloWorld();)
DEF_SAMPLE(return new TextSample_Align_Dir();)

View File

@ -0,0 +1,20 @@
# Copyright 2021 Google LLC.
# Things are easiest for everyone if these source paths are absolute.
_src = get_path_info("src", "abspath")
_include = get_path_info("include", "abspath")
sktext_public = [
"$_include/Processor.h",
"$_include/Types.h",
]
sktext_sources = [
"$_src/Decorator.cpp",
"$_src/Formatter.cpp",
"$_src/Line.cpp",
"$_src/Processor.cpp",
"$_src/Shaper.cpp",
"$_src/TextRun.cpp",
"$_src/Wrapper.cpp",
]

View File

@ -0,0 +1,15 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/Decorator.h"
namespace skia {
namespace text {
bool Decorator::process() {
// TODO: Decorate the text
return true;
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,14 @@
// Copyright 2021 Google LLC.
#ifndef Decorator_DEFINED
#define Decorator_DEFINED
#include "experimental/sktext/include/Processor.h"
namespace skia {
namespace text {
class Decorator {
bool process();
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,15 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/Formatter.h"
namespace skia {
namespace text {
bool Formatter::process() {
// TODO: Format the text by lines
return true;
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,21 @@
// Copyright 2021 Google LLC.
#ifndef Formatter_DEFINED
#define Formatter_DEFINED
#include "experimental/sktext/include/Processor.h"
namespace skia {
namespace text {
class Formatter {
public:
Formatter(Processor* processor, TextFormatStyle formatStyle) : fProcessor(processor), fFormatStyle(formatStyle) { }
bool process();
private:
Processor* fProcessor;
TextFormatStyle fFormatStyle;
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,50 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Processor.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/TextRun.h"
#include "modules/skshaper/src/SkUnicode.h"
namespace skia {
namespace text {
Line::Line(Processor* processor, const Stretch& stretch, const Stretch& spaces)
: fTextStart(stretch.fGlyphStart)
, fTextEnd(stretch.fGlyphEnd)
, fWhitespacesEnd (spaces.fGlyphEnd)
, fText(stretch.fTextRange)
, fWhitespaces(spaces.fTextRange)
, fTextWidth(stretch.fWidth)
, fSpacesWidth(spaces.fWidth) {
SkASSERT(stretch.isEmpty() ||
spaces.isEmpty() ||
(stretch.fGlyphEnd == spaces.fGlyphStart));
if (!stretch.isEmpty()) {
this->fTextMetrics.merge(stretch.fTextMetrics);
}
if (!spaces.isEmpty()) {
this->fTextMetrics.merge(spaces.fTextMetrics);
}
// This is just chosen to catch the common/fast cases. Feel free to tweak.
constexpr int kPreallocCount = 4;
auto start = stretch.fGlyphStart.fRunIndex;
auto end = spaces.fGlyphEnd.fRunIndex;
auto numRuns = end - start + 1;
SkAutoSTArray<kPreallocCount, SkUnicode::BidiLevel> runLevels(numRuns);
size_t runLevelsIndex = 0;
for (auto runIndex = start; runIndex <= end; ++runIndex) {
auto& run = processor->run(runIndex);
runLevels[runLevelsIndex++] = run.bidiLevel();
}
SkASSERT(runLevelsIndex == numRuns);
SkAutoSTArray<kPreallocCount, int32_t> logicalOrder(numRuns);
processor->getUnicode()->reorderVisual(runLevels.data(), numRuns, logicalOrder.data());
auto firstRunIndex = start;
for (auto index : logicalOrder) {
fRunsInVisualOrder.push_back(firstRunIndex + index);
}
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,162 @@
// Copyright 2021 Google LLC.
#ifndef TextLine_DEFINED
#define TextLine_DEFINED
#include "experimental/sktext/include/Types.h"
#include "include/core/SkFont.h"
#include "include/core/SkFontMetrics.h"
namespace skia {
namespace text {
class Processor;
class TextMetrics {
public:
TextMetrics() {
clean();
}
TextMetrics(const SkFont& font) {
SkFontMetrics metrics;
font.getMetrics(&metrics);
fAscent = metrics.fAscent;
fDescent = metrics.fDescent;
fLeading = metrics.fLeading;
}
TextMetrics(const TextMetrics&) = default;
void merge(TextMetrics tail) {
this->fAscent = std::min(this->fAscent, tail.fAscent);
this->fDescent = std::max(this->fDescent, tail.fDescent);
this->fLeading = std::max(this->fLeading, tail.fLeading);
}
void clean() {
this->fAscent = 0;
this->fDescent = 0;
this->fLeading = 0;
}
SkScalar height() const {
return this->fDescent - this->fAscent + this->fLeading;
}
SkScalar baseline() const {
return - this->fAscent + this->fLeading / 2;
}
private:
SkScalar fAscent;
SkScalar fDescent;
SkScalar fLeading;
};
class GlyphPos {
public:
GlyphPos() { }
GlyphPos(size_t runIndex, size_t glyphIndex) : fRunIndex(runIndex), fGlyphIndex(glyphIndex) { }
bool operator==(const GlyphPos& other) const {
return this->fRunIndex == other.fRunIndex && this->fGlyphIndex == other.fGlyphIndex;
}
size_t fRunIndex;
size_t fGlyphIndex;
};
class Stretch {
public:
Stretch() : fGlyphStart(), fGlyphEnd(), fWidth(0), fTextRange(0, 0), fTextMetrics(), fEmpty(true) { }
Stretch(GlyphPos glyphStart, size_t textIndex, const TextMetrics& metrics)
: fGlyphStart(glyphStart)
, fGlyphEnd(glyphStart)
, fWidth(0.0)
, fTextRange(textIndex, textIndex)
, fTextMetrics(metrics)
, fEmpty(false) { }
Stretch(Stretch&&) = default;
Stretch& operator=(Stretch&&) = default;
bool isEmpty() const {
return this->fEmpty/* ||
(this->fGlyphStart.fRunIndex == this->fGlyphEnd.fRunIndex &&
this->fGlyphStart.fGlyphIndex == this->fGlyphEnd.fGlyphIndex)*/;
}
void clean() {
this->fEmpty = true;
}
void moveTo(Stretch& tail) {
if (tail.fEmpty) {
return;
}
if (this->fEmpty) {
if (!tail.fEmpty) {
this->fGlyphStart = tail.fGlyphStart;
this->fGlyphEnd = tail.fGlyphEnd;
this->fWidth = tail.fWidth;
this->fTextRange = tail.fTextRange;
this->fTextMetrics = tail.fTextMetrics;
this->fEmpty = tail.fEmpty;
}
tail.clean();
return;
}
SkASSERT(this->fGlyphEnd.fRunIndex != tail.fGlyphStart.fRunIndex ||
this->fGlyphEnd.fGlyphIndex == tail.fGlyphStart.fGlyphIndex);
this->fGlyphEnd = tail.fGlyphEnd;
this->fWidth += tail.fWidth;
this->fTextRange.merge(tail.fTextRange);
this->fTextMetrics.merge(tail.fTextMetrics);
tail.clean();
}
void finish(size_t glyphIndex, size_t textIndex, SkScalar width) {
this->fTextRange.fEnd = textIndex;
this->fGlyphEnd.fGlyphIndex = glyphIndex;
this->fWidth = width;
}
GlyphPos fGlyphStart;
GlyphPos fGlyphEnd;
SkScalar fWidth;
Range fTextRange;
TextMetrics fTextMetrics;
bool fEmpty;
};
class Line {
public:
Line(Processor* processor, const Stretch& stretch, const Stretch& spaces);
~Line() = default;
private:
friend class Processor;
GlyphPos fTextStart;
GlyphPos fTextEnd ;
GlyphPos fWhitespacesEnd;
Range fText;
Range fWhitespaces;
SkScalar fTextWidth;
SkScalar fSpacesWidth;
TextMetrics fTextMetrics;
SkSTArray<1, size_t, true> fRunsInVisualOrder;
Processor* fProcessor;
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,296 @@
// Copyright 2021 Google LLC.
#include <stack>
#include "experimental/sktext/include/Processor.h"
#include "experimental/sktext/src/Formatter.h"
#include "experimental/sktext/src/Shaper.h"
#include "experimental/sktext/src/Wrapper.h"
namespace skia {
namespace text {
// The result of shaping is a set of Runs placed on one endless line
// It has all the glyph information
bool Processor::shape(TextFontStyle fontStyle, SkTArray<FontBlock, true> fontBlocks) {
if (fUnicode == nullptr) {
return false;
}
fFontBlocks = std::move(fontBlocks);
Shaper shaper(this, fontStyle);
if (!shaper.process()) {
return false;
}
this->markGlyphs();
return true;
}
// TODO: we can wrap to any shape, not just a rectangle
// The result of wrapping is a set of Lines that fit the required sizes and
// contain all the glyph information
bool Processor::wrap(SkScalar width, SkScalar height) {
Wrapper wrapper(this, width, height);
if (!wrapper.process()) {
return false;
}
return true;
}
// The result of formatting is a possible shift of glyphs as the format type requires
bool Processor::format(TextFormatStyle formatStyle) {
Formatter formatter(this, formatStyle);
if (!formatter.process()) {
return false;
}
return true;
}
// Once the text is decorated you can iterate it by segments (intersect of run, decor block and line)
bool Processor::decorate(SkTArray<DecorBlock, true> decorBlocks) {
this->iterateByVisualOrder(decorBlocks,
[&](SkSize offset, SkScalar baseline, const TextRun* run, Range textRange, Range glyphRange, const DecorBlock& block) {
SkTextBlobBuilder builder;
const auto& blobBuffer = builder.allocRunPos(run->fFont, SkToInt(glyphRange.width()));
sk_careful_memcpy(blobBuffer.glyphs, run->fGlyphs.data() + glyphRange.fStart, glyphRange.width() * sizeof(SkGlyphID));
sk_careful_memcpy(blobBuffer.points(), run->fPositions.data() + glyphRange.fStart, glyphRange.width() * sizeof(SkPoint));
offset.fHeight += baseline;
fTextOutputs.emplace_back(builder.make(), *block.fForegroundColor, offset);
});
return true;
}
// All at once
bool Processor::drawText(const char* text, SkCanvas* canvas, SkScalar x, SkScalar y) {
return drawText(text, canvas, TextFormatStyle(TextAlign::kLeft, TextDirection::kLtr), SK_ColorBLACK, SK_ColorWHITE, SkString("Roboto"), 14, SkFontStyle::Normal(), x, y);
}
bool Processor::drawText(const char* text, SkCanvas* canvas, TextFormatStyle textFormat, SkColor foreground, SkColor background, const SkString& fontFamily, SkScalar fontSize, SkFontStyle fontStyle, SkScalar x, SkScalar y) {
SkString str(text);
Range textRange(0, str.size());
Processor processor(str);
if (!processor.computeCodeUnitProperties()) {
return false;
}
if (!processor.shape({ textFormat.fDefaultTextDirection, SkFontMgr::RefDefault()}, {{{ fontFamily, fontSize, fontStyle, textRange }}})) {
return false;
}
if (!processor.wrap(SK_ScalarInfinity, SK_ScalarInfinity)) {
return false;
}
if (!processor.format(textFormat)) {
return false;
}
SkTArray<DecorBlock, true> decor;
SkPaint backgroundPaint; backgroundPaint.setColor(background);
SkPaint foregroundPaint; foregroundPaint.setColor(foreground);
if (!processor.decorate({{{&foregroundPaint, &backgroundPaint, textRange}}})) {
return false;
}
for (auto& output : processor.fTextOutputs) {
canvas->drawTextBlob(output.fTextBlob, x + output.fOffset.fWidth, y + output.fOffset.fHeight, output.fPaint);
}
return true;
}
// Also adjust the decoration block edges to cluster edges while we at it
// to avoid an enormous amount of complications
void Processor::sortDecorBlocks(SkTArray<DecorBlock, true>& decorBlocks) {
// Soft the blocks
std::sort(decorBlocks.begin(), decorBlocks.end(),
[](const DecorBlock& a, const DecorBlock& b) {
return a.fRange.fStart < b.fRange.fStart;
});
// Walk through the blocks using the default when missing
SkPaint* foreground = new SkPaint();
foreground->setColor(SK_ColorBLACK);
std::stack<DecorBlock> defaultBlocks;
defaultBlocks.emplace(foreground, nullptr, Range(0, fText.size()));
size_t start = 0;
for (auto& block : decorBlocks) {
this->adjustLeft(&block.fRange.fStart);
this->adjustLeft(&block.fRange.fEnd);
SkASSERT(start <= block.fRange.fStart);
if (start < block.fRange.fStart) {
auto defaultBlock = defaultBlocks.top();
decorBlocks.emplace_back(defaultBlock.fForegroundColor, defaultBlock.fBackgroundColor, Range(start, block.fRange.fStart));
}
start = block.fRange.fEnd;
while (!defaultBlocks.empty()) {
auto defaultBlock = defaultBlocks.top();
if (defaultBlock.fRange.fEnd <= block.fRange.fEnd) {
defaultBlocks.pop();
}
}
defaultBlocks.push(block);
}
if (start < fText.size()) {
auto defaultBlock = defaultBlocks.top();
decorBlocks.emplace_back(defaultBlock.fForegroundColor, defaultBlock.fBackgroundColor, Range(start, fText.size()));
}
}
bool Processor::computeCodeUnitProperties() {
fCodeUnitProperties.push_back_n(fText.size() + 1, CodeUnitFlags::kNoCodeUnitFlag);
fUnicode = std::move(SkUnicode::Make());
if (nullptr == fUnicode) {
return false;
}
// Get white spaces
fUnicode->forEachCodepoint(fText.c_str(), fText.size(),
[this](SkUnichar unichar, int32_t start, int32_t end) {
if (fUnicode->isWhitespace(unichar)) {
for (auto i = start; i < end; ++i) {
fCodeUnitProperties[i] |= CodeUnitFlags::kPartOfWhiteSpace;
}
}
});
// Get line breaks
std::vector<SkUnicode::LineBreakBefore> lineBreaks;
if (!fUnicode->getLineBreaks(fText.c_str(), fText.size(), &lineBreaks)) {
return false;
}
for (auto& lineBreak : lineBreaks) {
fCodeUnitProperties[lineBreak.pos] |= lineBreak.breakType == SkUnicode::LineBreakType::kHardLineBreak
? CodeUnitFlags::kHardLineBreakBefore
: CodeUnitFlags::kSoftLineBreakBefore;
}
// Get graphemes
std::vector<SkUnicode::Position> graphemes;
if (!fUnicode->getGraphemes(fText.c_str(), fText.size(), &graphemes)) {
return false;
}
for (auto pos : graphemes) {
fCodeUnitProperties[pos] |= CodeUnitFlags::kGraphemeStart;
}
return true;
}
void Processor::markGlyphs() {
for (auto& run : fRuns) {
for (auto index : run.fClusters) {
fCodeUnitProperties[index] |= CodeUnitFlags::kGlyphStart;
}
}
}
template<typename Visitor>
void Processor::iterateByLogicalOrder(CodeUnitFlags units, Visitor visitor) {
Range range(0, 0);
for (size_t index = 0; index < fCodeUnitProperties.size(); ++index) {
if (this->hasProperty(index, units)) {
range.fEnd = index;
visitor(range, this->fCodeUnitProperties[index]);
range.fStart = index;
}
}
}
template<typename Visitor>
void Processor::iterateByVisualOrder(CodeUnitFlags units, Visitor visitor) {
SkSize offset = SkSize::MakeEmpty();
for (auto& line : fLines) {
offset.fWidth = 0;
for (auto& runIndex : line.fRunsInVisualOrder) {
auto& run = fRuns[runIndex];
auto startGlyph = runIndex == line.fTextStart.fRunIndex == runIndex ? line.fTextStart.fGlyphIndex : 0;
auto endGlyph = runIndex == line.fTextEnd.fRunIndex ? line.fTextEnd.fGlyphIndex : run.fGlyphs.size();
Range textRange(run.fUtf8Range.begin(), run.fUtf8Range.end());
Range glyphRange(startGlyph, endGlyph);
for (auto glyphIndex = startGlyph; glyphIndex <= endGlyph; ++glyphIndex) {
auto textIndex = run.fClusters[glyphIndex];
if (glyphIndex < endGlyph && !this->hasProperty(textIndex, units)) {
continue;
}
textRange.fEnd = textIndex;
glyphRange.fEnd = glyphIndex;
visitor(offset, line.fTextMetrics.baseline(), &run, textRange, glyphRange, this->fCodeUnitProperties[textIndex]);
textRange.fStart = textIndex;
glyphRange.fStart = glyphIndex;
offset.fWidth += run.calculateWidth(glyphRange);
}
}
offset.fHeight += line.fTextMetrics.height();
}
}
template<typename Visitor>
void Processor::iterateByVisualOrder(SkTArray<DecorBlock, true>& decorBlocks, Visitor visitor) {
this->sortDecorBlocks(decorBlocks);
// Decor blocks have to be sorted by text cannot intersect but can skip some parts of the text
// (in which case we use default text style from paragraph style)
// The edges of the decor blocks don't have to match glyph, grapheme or even unicode code point edges
// It's out responsibility to adjust them to some reasonable values
// [a:b) -> [c:d) where
// c is closest GG cluster edge to a from the left and d is closest GG cluster edge to b from the left
DecorBlock* currentBlock = &decorBlocks[0];
SkSize offset = SkSize::MakeEmpty();
for (auto& line : fLines) {
offset.fWidth = 0;
for (auto& runIndex : line.fRunsInVisualOrder) {
auto& run = fRuns[runIndex];
// The run edges are good (aligned to GGC)
// "ABCdef" -> "defCBA"
// "AB": red
// "Cd": green
// "ef": blue
// green[d] blue[ef] green [C] red [BA]
auto startGlyph = runIndex == line.fTextStart.fRunIndex == runIndex ? line.fTextStart.fGlyphIndex : 0;
auto endGlyph = runIndex == line.fTextEnd.fRunIndex ? line.fTextEnd.fGlyphIndex : run.fGlyphs.size();
Range textRange(run.fUtf8Range.begin(), run.fUtf8Range.end());
Range glyphRange(startGlyph, endGlyph);
SkASSERT(currentBlock->fRange.fStart <= textRange.fStart);
for (auto glyphIndex = startGlyph; glyphIndex <= endGlyph; ++glyphIndex) {
auto textIndex = run.fClusters[glyphIndex];
if (run.leftToRight() && textIndex < currentBlock->fRange.fEnd) {
continue;
} else if (!run.leftToRight() && textIndex > currentBlock->fRange.fStart) {
continue;
}
if (run.leftToRight()) {
textRange.fEnd = textIndex;
} else {
textRange.fStart = textIndex;
}
glyphRange.fEnd = glyphIndex;
visitor(offset, line.fTextMetrics.baseline(), &run, textRange, glyphRange, *currentBlock);
if (run.leftToRight()) {
textRange.fStart = textIndex;
} else {
textRange.fEnd = textIndex;
}
glyphRange.fStart = glyphIndex;
offset.fWidth += run.calculateWidth(glyphRange);
}
}
offset.fHeight += line.fTextMetrics.height();
}
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,71 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Processor.h"
#include "experimental/sktext/src/Shaper.h"
namespace skia {
namespace text {
// TODO: calculate intrinsic sizes
// Shape the text in one line
bool Shaper::process() {
auto text(fProcessor->fText);
for (auto& block : fProcessor->fFontBlocks) {
SkFont font(this->createFont(block));
SkShaper::TrivialFontRunIterator fontIter(font, text.size());
SkShaper::TrivialLanguageRunIterator langIter(text.c_str(), text.size());
std::unique_ptr<SkShaper::BiDiRunIterator> bidiIter(
SkShaper::MakeSkUnicodeBidiRunIterator(
fProcessor->fUnicode.get(), text.c_str(), text.size(), fDefaultTextDirection == TextDirection::kLtr ? 0 : 1));
std::unique_ptr<SkShaper::ScriptRunIterator> scriptIter(
SkShaper::MakeSkUnicodeHbScriptRunIterator(fProcessor->fUnicode.get(), text.c_str(), text.size()));
auto shaper = SkShaper::MakeShapeDontWrapOrReorder();
if (shaper == nullptr) {
// For instance, loadICU does not work. We have to stop the process
return false;
}
shaper->shape(text.c_str(), text.size(),
fontIter, *bidiIter, *scriptIter, langIter,
std::numeric_limits<SkScalar>::max(), this);
}
return true;
}
void Shaper::commitRunBuffer(const RunInfo&) {
fCurrentRun->commit();
fProcessor->fRuns.emplace_back(std::move(*fCurrentRun));
}
SkFont Shaper::createFont(const FontBlock& block) {
sk_sp<SkTypeface> typeface = matchTypeface(block.fFontFamily, block.fFontStyle);
SkFont font(std::move(typeface), block.fFontSize);
font.setEdging(SkFont::Edging::kAntiAlias);
font.setHinting(SkFontHinting::kSlight);
font.setSubpixel(true);
return font;
}
sk_sp<SkTypeface> Shaper::matchTypeface(const SkString& fontFamily, SkFontStyle fontStyle) {
sk_sp<SkFontStyleSet> set(fFontManager->matchFamily(fontFamily.c_str()));
if (!set || set->count() == 0) {
return nullptr;
}
sk_sp<SkTypeface> match(set->matchStyle(fontStyle));
if (match) {
return match;
}
return nullptr;
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,45 @@
// Copyright 2021 Google LLC.
#ifndef Shaper_DEFINED
#define Shaper_DEFINED
#include "experimental/sktext/src/TextRun.h"
#include "modules/skshaper/include/SkShaper.h"
namespace skia {
namespace text {
class Processor;
class Shaper : public SkShaper::RunHandler{
public:
Shaper(Processor* processor, TextFontStyle textFontStyle)
: fProcessor(processor)
, fFontManager(textFontStyle.fFontManager)
, fDefaultTextDirection(textFontStyle.fTextDirection)
, fCurrentRun(nullptr) { }
bool process();
private:
SkFont createFont(const FontBlock& block);
sk_sp<SkTypeface> matchTypeface(const SkString& fontFamily, SkFontStyle fontStyle);
void beginLine() override {}
void runInfo(const RunInfo&) override {}
void commitRunInfo() override {}
void commitLine() override {}
Buffer runBuffer(const RunInfo& info) override {
fCurrentRun = std::make_unique<TextRun>(info);
return fCurrentRun->newRunBuffer();
}
void commitRunBuffer(const RunInfo&) override;
Processor* fProcessor;
sk_sp<SkFontMgr> fFontManager;
TextDirection fDefaultTextDirection;
std::unique_ptr<TextRun> fCurrentRun;
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,43 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/TextRun.h"
namespace skia {
namespace text {
class Processor;
TextRun::TextRun(const SkShaper::RunHandler::RunInfo& info)
: fFont(info.fFont) {
fBidiLevel = info.fBidiLevel;
fAdvance = info.fAdvance;
fUtf8Range = info.utf8Range;
fGlyphs.push_back_n(info.glyphCount);
fBounds.push_back_n(info.glyphCount);
fPositions.push_back_n(info.glyphCount + 1);
fClusters.push_back_n(info.glyphCount + 1);
// To make edge cases easier:
fPositions[info.glyphCount] = fAdvance;
fClusters[info.glyphCount] =
leftToRight() ? info.utf8Range.end() : info.utf8Range.begin();
}
void TextRun::commit() {
fFont.getBounds(fGlyphs.data(), fGlyphs.size(), fBounds.data(), nullptr);
// To make edge cases easier
fPositions[fGlyphs.size()] = fAdvance;
fClusters[fGlyphs.size()] = this->leftToRight() ? fUtf8Range.end() : fUtf8Range.begin();
}
SkShaper::RunHandler::Buffer TextRun::newRunBuffer() {
return {fGlyphs.data(), fPositions.data(), nullptr, fClusters.data(), {0.0f, 0.0f} };
}
SkScalar TextRun::calculateWidth(Range glyphRange) const {
SkASSERT(glyphRange.fStart <= glyphRange.fEnd && glyphRange.fEnd < fPositions.size());
return fPositions[glyphRange.fEnd].fX - fPositions[glyphRange.fStart].fX;
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,43 @@
// Copyright 2021 Google LLC.
#ifndef TextRun_DEFINED
#define TextRun_DEFINED
#include "experimental/sktext/include/Types.h"
#include "modules/skshaper/include/SkShaper.h"
namespace skia {
namespace text {
class TextRun {
public:
TextRun(const SkShaper::RunHandler::RunInfo& info);
TextRun& operator=(const TextRun&) = delete;
TextRun(TextRun&&) = default;
TextRun& operator=(TextRun&&) = delete;
~TextRun() = default;
SkShaper::RunHandler::Buffer newRunBuffer();
void commit();
SkScalar calculateWidth(Range glyphRange) const;
bool leftToRight() const { return fBidiLevel % 2 == 0; }
uint8_t bidiLevel() const { return fBidiLevel; }
private:
friend class Wrapper;
friend class Processor;
SkFont fFont;
SkVector fAdvance;
SkShaper::RunHandler::Range fUtf8Range;
SkSTArray<128, SkGlyphID, true> fGlyphs;
SkSTArray<128, SkPoint, true> fPositions;
SkSTArray<128, uint32_t, true> fClusters;
SkSTArray<128, SkRect, true> fBounds;
uint8_t fBidiLevel;
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,231 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/Wrapper.h"
namespace skia {
namespace text {
bool Wrapper::process() {
// TODO: define max line number based on this->fHeight
return this->breakTextIntoLines(this->fWidth);
}
Range Wrapper::glyphRange(const TextRun* run, const Range& textRange) {
Range glyphRange = EMPTY_RANGE;
for (size_t i = 0; i < run->fClusters.size(); ++i) {
auto cluster = run->fClusters[i];
if (cluster < textRange.fStart) continue;
if (cluster > textRange.fEnd) break;
if (glyphRange.fStart == EMPTY_INDEX) {
glyphRange.fStart = i;
}
glyphRange.fEnd = i;
}
return glyphRange;
}
Range Wrapper::textRange(const TextRun* run, const Range& glyphRange) {
Range textRange = EMPTY_RANGE;
for (size_t i = 0; i < run->fClusters.size(); ++i) {
auto cluster = run->fClusters[i];
if (i < glyphRange.fStart) continue;
if (i > glyphRange.fEnd) break;
if (textRange.fStart == EMPTY_INDEX) {
textRange.fStart = cluster;
}
textRange.fEnd = cluster;
}
return textRange;
}
bool Wrapper::breakTextIntoLines(SkScalar width) {
// line : spaces : clusters
Stretch line;
Stretch spaces;
Stretch clusters;
for (size_t runIndex = 0; runIndex < fProcessor->fRuns.size(); ++runIndex ) {
auto& run = fProcessor->fRuns[runIndex];
TextMetrics runMetrics(run.fFont);
Stretch cluster;
if (!run.leftToRight()) {
cluster.fTextRange = { run.fUtf8Range.end(), run.fUtf8Range.end()};
}
for (size_t glyphIndex = 0; glyphIndex < run.fPositions.size(); ++glyphIndex) {
auto textIndex = run.fClusters[glyphIndex];
if (cluster.isEmpty()) {
cluster = Stretch(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics);
continue;
/*
} else if (cluster.fTextRange.fStart == textIndex) {
break;
} else if (cluster.fTextRange.fEnd == textIndex) {
break;
*/
}
// The entire cluster belongs to a single run
SkASSERT(cluster.fGlyphStart.fRunIndex == runIndex);
auto clusterWidth = run.fPositions[glyphIndex].fX - run.fPositions[cluster.fGlyphStart.fGlyphIndex].fX;
cluster.finish(glyphIndex, textIndex, clusterWidth);
auto isHardLineBreak = fProcessor->isHardLineBreak(cluster.fTextRange.fStart);
auto isSoftLineBreak = fProcessor->isSoftLineBreak(cluster.fTextRange.fStart);
auto isWhitespaces = fProcessor->isWhitespaces(cluster.fTextRange);
auto isEndOfText = run.leftToRight() ? textIndex == run.fUtf8Range.end() : textIndex == run.fUtf8Range.begin();
if (isHardLineBreak || isEndOfText || isSoftLineBreak || isWhitespaces) {
if (!clusters.isEmpty()) {
line.moveTo(spaces);
line.moveTo(clusters);
}
if (isWhitespaces) {
spaces.moveTo(cluster);
}
if (isHardLineBreak) {
this->addLine(line, spaces);
break;
}
if (isEndOfText) {
line.moveTo(cluster);
if (!line.isEmpty()) {
this->addLine(line, spaces);
}
break;
}
}
if (!SkScalarIsFinite(width)) {
clusters.moveTo(cluster);
} else if ((line.fWidth + spaces.fWidth + clusters.fWidth + cluster.fWidth) <= width) {
clusters.moveTo(cluster);
} else {
// Wrapping the text by whitespaces
if (line.isEmpty()) {
if (clusters.isEmpty()) {
line.moveTo(cluster);
} else {
line.moveTo(clusters);
}
}
this->addLine(line, spaces);
clusters.moveTo(cluster);
}
cluster = Stretch(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics);
}
}
return true;
}
/*
type SingleRunSingleStyleCallback = (style, glyphRange) => number;
breakRunIntoStyles(inputs, styleFlags : TextStyleFlagsEnum, runStart, run, glyphRange, callback: SingleRunSingleStyleCallback) {
let lastFlags = TextStyleFlagsEnum.kNoStyle;
let styleRange;
let index = 0;
for (let flags of this.fTextStyles) {
let currentFlags = lastFlags = flags & styleFlags;
if (currentFlags == lastFlags) {
styleRange.end = index;
} else {
callback();
lastFlags = currentFlags;
styleRange = { start: index, end: index};
}
}
const textRange = this.glyphRange(run, glyphRange);
for (let block of inputs.blocks) {
const intersect = this.intersect(block.textRange, textRange);
if (this.width(intersect) == 0) {
continue;
}
const glyphRange = this.glyphRange(run, intersect);
runStart += callback(block.style, glyphRange);
}
}
generateDrawingOperations(inputs, outputs) {
const textLayout = this;
// TODO: Sort runs on the line accordingly to the visual order (for LTR/RTL mixture)
// Iterate through all the runs on the line
let runStart = 0.0;
let lineVerticalStart = 0.0;
this.fLines.forEach(function (line, lineIndex) {
var run = null;
var lastRunIndex;
for (var runIndex = line.textStart.runIndex; runIndex <= line.textEnd.runIndex; ++runIndex) {
if (run != null && lastRunIndex != runIndex) {
continue;
}
run = inputs.runs[runIndex];
lastRunIndex = runIndex;
let runGlyphRange = {
start: line.textStart.runIndex ? line.textStart.glyphIndex : 0,
end: line.textEnd.runIndex ? line.textEnd.glyphIndex : run.glyphs.size()
}
// Iterate through all the styles in the run
// TODO: For now assume that the style edges are cluster edges and don't fall between clusters
// TODO: Just show the text, not decorations of any kind
textLayout.breakRunIntoStyles(inputs, runStart, run, runGlyphRange, function(style, glyphRange) {
// left, top, right, bottom
let runWidth = textLayout.glyphRangeWidth(run, glyphRange);
if (style.background == undefined) {
return runWidth;
}
let rectangle = {
backgroundColor: style.background,
left: runStart,
top: lineVerticalStart,
width: runWidth,
height: run.font.metrics.ascent + run.font.metrics.descent + run.font.metrics.leading
};
outputs.rectangles.push(rectangle);
return runWidth;
});
textLayout.breakRunIntoStyles(inputs, runStart, run, runGlyphRange, function(style, glyphRange) {
// TODO: Ignore clipping cluster by position for now
let textBlob = {
foregroundColor: style.foreground,
run : runIndex,
glyphRange: glyphRange,
shift: {
x: runStart,
y: lineVerticalStart
}
};
outputs.textBlobs.push(textBlob);
return textLayout.glyphRangeWidth(run, glyphRange);
});
}
});
}
}
*/
} // namespace text
} // namespace skia

View File

@ -0,0 +1,40 @@
// Copyright 2021 Google LLC.
#ifndef Wrapper_DEFINED
#define Wrapper_DEFINED
#include "experimental/sktext/include/Processor.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/TextRun.h"
namespace skia {
namespace text {
class Wrapper {
public:
Wrapper(Processor* processor, SkScalar width, SkScalar height) : fProcessor(processor), fWidth(width), fHeight(height) { }
bool process();
void addLine(Stretch& stretch, Stretch& spaces) {
fProcessor->fLines.emplace_back(fProcessor, stretch, spaces);
stretch.clean();
spaces.clean();
}
SkScalar glyphRangeWidth(const TextRun* run, const Range& glyphRange) {
return run->fPositions[glyphRange.fEnd].fX - run->fPositions[glyphRange.fStart].fX;
}
static Range glyphRange(const TextRun* run, const Range& textRange);
static Range textRange(const TextRun* run, const Range& glyphRange);
bool breakTextIntoLines(SkScalar width);
private:
Processor* fProcessor;
SkScalar fWidth;
SkScalar fHeight;
};
} // namespace text
} // namespace skia
#endif