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:
parent
5276ba274b
commit
159fd844c3
6
BUILD.gn
6
BUILD.gn
@ -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",
|
||||
|
96
experimental/sktext/BUILD.gn
Normal file
96
experimental/sktext/BUILD.gn
Normal 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") {
|
||||
}
|
||||
}
|
200
experimental/sktext/include/Processor.h
Normal file
200
experimental/sktext/include/Processor.h
Normal 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
|
127
experimental/sktext/include/Types.h
Normal file
127
experimental/sktext/include/Types.h
Normal 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
|
122
experimental/sktext/samples/Text.cpp
Normal file
122
experimental/sktext/samples/Text.cpp
Normal 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();)
|
20
experimental/sktext/sktext.gni
Normal file
20
experimental/sktext/sktext.gni
Normal 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",
|
||||
]
|
15
experimental/sktext/src/Decorator.cpp
Normal file
15
experimental/sktext/src/Decorator.cpp
Normal 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
|
14
experimental/sktext/src/Decorator.h
Normal file
14
experimental/sktext/src/Decorator.h
Normal 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
|
15
experimental/sktext/src/Formatter.cpp
Normal file
15
experimental/sktext/src/Formatter.cpp
Normal 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
|
21
experimental/sktext/src/Formatter.h
Normal file
21
experimental/sktext/src/Formatter.h
Normal 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
|
50
experimental/sktext/src/Line.cpp
Normal file
50
experimental/sktext/src/Line.cpp
Normal 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
|
162
experimental/sktext/src/Line.h
Normal file
162
experimental/sktext/src/Line.h
Normal 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
|
296
experimental/sktext/src/Processor.cpp
Normal file
296
experimental/sktext/src/Processor.cpp
Normal 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
|
71
experimental/sktext/src/Shaper.cpp
Normal file
71
experimental/sktext/src/Shaper.cpp
Normal 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
|
45
experimental/sktext/src/Shaper.h
Normal file
45
experimental/sktext/src/Shaper.h
Normal 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
|
43
experimental/sktext/src/TextRun.cpp
Normal file
43
experimental/sktext/src/TextRun.cpp
Normal 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
|
43
experimental/sktext/src/TextRun.h
Normal file
43
experimental/sktext/src/TextRun.h
Normal 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
|
231
experimental/sktext/src/Wrapper.cpp
Normal file
231
experimental/sktext/src/Wrapper.cpp
Normal 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
|
40
experimental/sktext/src/Wrapper.h
Normal file
40
experimental/sktext/src/Wrapper.h
Normal 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
|
Loading…
Reference in New Issue
Block a user