Reland "The very first version of a new SkText API."

This reverts commit efb7ae16dd.

Reason for revert: Excluding all non-linux tests

Original change's description:
> Revert "The very first version of a new SkText API."
>
> This reverts commit 8a20cea604.
>
> Reason for revert: Breaking some tests
>
> Original change's description:
> > The very first version of a new SkText API.
> >
> > SkText public API added.
> > Interface.h updated.
> >
> > Change-Id: I82a87f33e6cf1394fa2520387f6895d33601376e
> > Reviewed-on: https://skia-review.googlesource.com/c/skia/+/442003
> > Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
> > Commit-Queue: Julia Lavrova <jlavrova@google.com>
>
> Change-Id: I879b198bc6fb44c46d11d967118f8d69eb74fee0
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/449296
> Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
> Reviewed-by: John Stiles <johnstiles@google.com>
> Commit-Queue: Julia Lavrova <jlavrova@google.com>

Change-Id: Ieb65b45532d8c7455f091cf7a9e21fa7dea47606
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/449297
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Reviewed-by: John Stiles <johnstiles@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
This commit is contained in:
Julia Lavrova 2021-09-15 20:09:32 +00:00 committed by SkCQ
parent c915da9b20
commit 9d0d426028
25 changed files with 2182 additions and 1206 deletions

View File

@ -47,7 +47,12 @@ if (skia_enable_sktext && skia_enable_skshaper && skia_use_icu &&
source_set("tests") {
if (text_tests_enabled) {
testonly = true
sources = []
sources = [
"tests/SelectableText.cpp",
"tests/ShapedText.cpp",
"tests/UnicodeText.cpp",
"tests/WrappedText.cpp",
]
deps = [
":sktext",
"../..:gpu_tool_utils",

View File

@ -47,19 +47,19 @@ Editor::Editor(std::u16string text, SkSize size)
// In order to get that position we look for a position outside of the text
// and that will give us the last glyph on the line
auto endOfText = fEditableText->lastElement(fDefaultPositionType);
fEditableText->recalculateBoundaries(endOfText);
//fEditableText->recalculateBoundaries(endOfText);
fCursor->place(endOfText.fBoundaries);
}
void Editor::update() {
if (!fEditableText->isInvalidated()) {
if (fEditableText->isValid()) {
return;
}
// Update the (shift it to point at the grapheme edge)
auto position = fEditableText->adjustedPosition(fDefaultPositionType, fCursor->getCenterPosition());
fEditableText->recalculateBoundaries(position);
//fEditableText->recalculateBoundaries(position);
fCursor->place(position.fBoundaries);
// TODO: Update the mouse
@ -71,9 +71,7 @@ void Editor::update() {
bool Editor::moveCursor(skui::Key key) {
auto cursorPosition = fCursor->getCenterPosition();
auto position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition);
if (position.fRun == nullptr) {
return false;
}
if (key == skui::Key::kLeft) {
position = fEditableText->previousElement(position);
} else if (key == skui::Key::kRight) {
@ -87,19 +85,21 @@ bool Editor::moveCursor(skui::Key key) {
if (position.fLineIndex == 0) {
return false;
}
cursorPosition.offset(0, - fEditableText->lineHeight(position.fLineIndex));
auto prevLine = fEditableText->getLine(position.fLineIndex - 1);
cursorPosition.offset(0, - prevLine.fBounds.height());
position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition);
} else if (key == skui::Key::kDown) {
// Move one line down (if possible)
if (position.fLineIndex == fEditableText->lineCount() - 1) {
return false;
}
cursorPosition.offset(0, fEditableText->lineHeight(position.fLineIndex));
auto nextLine = fEditableText->getLine(position.fLineIndex + 1);
cursorPosition.offset(0, nextLine.fBounds.height());
position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition);
}
// Place the cursor at the new position
fEditableText->recalculateBoundaries(position);
//fEditableText->recalculateBoundaries(position);
fCursor->place(position.fBoundaries);
this->invalidate();
@ -157,28 +157,18 @@ bool Editor::deleteElement(skui::Key key) {
// IMPORTANT: We assume that a single element (grapheme cluster) does not cross the run boundaries;
// It's not exactly true but we are going to enforce in by breaking the grapheme by the run boundaries
if (key == skui::Key::kBack) {
// Move to the previous element (could be another run and/or line)
if (position.fLineIndex > 0 && fEditableText->isFirstOnTheLine(position)) {
auto line = fEditableText->line(position.fLineIndex - 1);
if (line->isHardLineBreak()) {
// We remove invisible hard line break from the previous line
textRange = line->whitespaces();
} else {
// We remove the last non-whitespace element on the previous line (ignoring trailing whitespaces)
position = fEditableText->previousElement(position);
textRange = position.fTextRange;
}
} else {
// We remove the previous element on the current line
position = fEditableText->previousElement(position);
textRange = position.fTextRange;
}
// TODO: Make sure previous element moves smoothly over the line break
position = fEditableText->previousElement(position);
textRange = position.fTextRange;
fCursor->place(position.fBoundaries);
} else {
// The cursor stays the the same place
}
fEditableText->removeElement(textRange);
// Find the grapheme the cursor points to
position = fEditableText->adjustedPosition(fDefaultPositionType, textRange.fStart);
position = fEditableText->adjustedPosition(fDefaultPositionType, SkPoint::Make(position.fBoundaries.fLeft, position.fBoundaries.fTop));
fCursor->place(position.fBoundaries);
this->invalidate();
@ -202,7 +192,7 @@ bool Editor::insertCodepoint(SkUnichar unichar) {
// Move the cursor to the next element
position = fEditableText->nextElement(position);
fEditableText->recalculateBoundaries(position);
//fEditableText->recalculateBoundaries(position);
fCursor->place(position.fBoundaries);
this->invalidate();

View File

@ -7,20 +7,43 @@ namespace skia {
namespace editor {
void DynamicText::paint(SkCanvas* canvas) {
if (!fDrawableText) {
auto chunks = this->getDecorationChunks(fDecorations);
fDrawableText = fWrappedText->prepareToDraw(fUnicodeText.get(),
PositionType::kGraphemeCluster,
SkSpan<TextIndex>(chunks.data(), chunks.size()));
}
Paint painter;
painter.paint(canvas, this->fOffset, this->fFormattedText.get(), this->fDecorations);
auto foregroundPaint = fDecorations[0].foregroundPaint;
auto textBlobs = fDrawableText->getTextBlobs();
for (auto& textBLob : textBlobs) {
canvas->drawTextBlob(textBLob, 0, 0, foregroundPaint);
}
}
std::vector<TextIndex> DynamicText::getDecorationChunks(SkSpan<DecoratedBlock> decorations) const {
std::vector<TextIndex> result;
TextIndex textIndex = 0;
for (auto& decoration : decorations) {
textIndex += decoration.charCount;
result.emplace_back(textIndex);
}
return result;
}
void EditableText::paint(SkCanvas* canvas) {
Paint painter;
if (fSelection->isEmpty()) {
painter.paint(canvas, this->fOffset, this->fFormattedText.get(), fDecorations);
DynamicText::paint(canvas);
} else {
auto decorations = mergeSelectionIntoDecorations();
painter.paint(canvas, this->fOffset, this->fFormattedText.get(), SkSpan<DecoratedBlock>(decorations.data(), decorations.size()));
auto chunks = this->getDecorationChunks(SkSpan<DecoratedBlock>(decorations.data(), decorations.size()));
fDrawableText = fWrappedText->prepareToDraw(fUnicodeText.get(), PositionType::kGraphemeCluster, SkSpan<TextIndex>(chunks.data(), chunks.size()));
}
auto foregroundPaint = fDecorations[0].foregroundPaint;
auto textBlobs = fDrawableText->getTextBlobs();
for (auto& textBLob : textBlobs) {
canvas->drawTextBlob(textBLob, 0, 0, foregroundPaint);
}
}
@ -51,7 +74,7 @@ SkTArray<DecoratedBlock> EditableText::mergeSelectionIntoDecorations() {
}
SkASSERT(decorPos == selected.fStart);
// So the next decoration intesects the selection (and the selection wins)
// So the next decoration intersects the selection (and the selection wins)
merged.emplace_back(selected.width(), fSelection->fForeground, fSelection->fBackground);
decorPos += selected.width();
SkASSERT(decorPos == selected.fEnd);

View File

@ -8,7 +8,6 @@
#include "experimental/sktext/editor/Selection.h"
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Paint.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTime.h"
@ -30,10 +29,12 @@ public:
fSize = size;
fFontBlocks = fontBlocks;
fText = std::move(text);
fUnicodeText = Text::parse(SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
auto unicode = SkUnicode::Make();
fUnicodeText = std::make_unique<UnicodeText>(std::move(unicode), SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
fShapedText = fUnicodeText->shape(fontBlocks, DEFAULT_TEXT_DIRECTION);
fWrappedText = fShapedText->wrap(size.width(), size.height(), fUnicodeText->getUnicode());
fFormattedText = fWrappedText->format(DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_DIRECTION);
fWrappedText = fShapedText->wrap(fUnicodeText.get(), size.width(), size.height());
fWrappedText->format(DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_DIRECTION);
}
virtual ~StaticText() = default;
@ -44,7 +45,6 @@ public:
std::unique_ptr<UnicodeText> fUnicodeText;
std::unique_ptr<ShapedText> fShapedText;
std::unique_ptr<WrappedText> fWrappedText;
sk_sp<FormattedText> fFormattedText;
SkSize fSize;
SkPoint fOffset;
SkSpan<FontBlock> fFontBlocks;
@ -65,11 +65,12 @@ public:
fTextAlign = textAlign;
fTextDirection = textDirection;
fText = std::move(text);
fUnicodeText = Text::parse(SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
auto unicode = SkUnicode::Make();
fUnicodeText = std::make_unique<UnicodeText>(std::move(unicode), SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
fShapedText = fUnicodeText->shape(fontBlocks, fTextDirection);
fWrappedText = fShapedText->wrap(size.width(), size.height(), fUnicodeText->getUnicode());
fFormattedText = fWrappedText->format(fTextAlign, fTextDirection);
fInvalidated = false;
fWrappedText = fShapedText->wrap(fUnicodeText.get(), size.width(), size.height());
fWrappedText->format(fTextAlign, fTextDirection);
}
virtual ~DynamicText() = default;
@ -78,8 +79,10 @@ public:
return SkRect::MakeXYWH(fOffset.fX, fOffset.fY, fRequiredSize.fWidth, fRequiredSize.fHeight).contains(x, y);
}
void invalidate() { fInvalidated = true; }
bool isInvalidated() { return fInvalidated; }
void invalidate() { fDrawableText = nullptr; }
bool isValid() { return fDrawableText != nullptr; }
std::vector<TextIndex> getDecorationChunks(SkSpan<DecoratedBlock> decorations) const;
bool rebuild(std::u16string text) {
if (!this->fFontBlocks.empty()) {
@ -87,18 +90,24 @@ public:
this->fFontBlocks[0].charCount = text.size();
}
this->fText = std::move(text);
this->fUnicodeText = Text::parse(SkSpan<uint16_t>((uint16_t*)this->fText.data(), this->fText.size()));
this->fShapedText = fUnicodeText->shape(this->fFontBlocks, fTextDirection);
this->fWrappedText = fShapedText->wrap(this->fRequiredSize.fWidth, this->fRequiredSize.fHeight, this->fUnicodeText->getUnicode());
this->fFormattedText = fWrappedText->format(fTextAlign, fTextDirection);
this->fActualSize = fFormattedText->actualSize();
this->fInvalidated = false;
auto unicode = SkUnicode::Make();
fUnicodeText = std::make_unique<UnicodeText>(std::move(unicode), SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
fShapedText = fUnicodeText->shape(this->fFontBlocks, fTextDirection);
fWrappedText = fShapedText->wrap(fUnicodeText.get(), this->fRequiredSize.fWidth, this->fRequiredSize.fHeight);
fWrappedText->format(fTextAlign, fTextDirection);
fSelectableText = fWrappedText->prepareToEdit(fUnicodeText.get());
fDrawableText = nullptr;
fActualSize = fWrappedText->actualSize();
return true;
}
SkScalar lineHeight(size_t index) const { return fFormattedText->line(index)->height(); }
size_t lineCount() const { return fFormattedText->countLines(); }
size_t lineCount() const { return fSelectableText->countLines(); }
BoxLine getLine(size_t lineIndex) {
SkASSERT(lineIndex < fSelectableText->countLines());
return fSelectableText->getLine(lineIndex);
}
SkRect actualSize() const {
return SkRect::MakeXYWH(fOffset.fX, fOffset.fY, fActualSize.fWidth, fActualSize.fHeight);
@ -111,7 +120,8 @@ protected:
std::unique_ptr<UnicodeText> fUnicodeText;
std::unique_ptr<ShapedText> fShapedText;
std::unique_ptr<WrappedText> fWrappedText;
sk_sp<FormattedText> fFormattedText;
std::unique_ptr<DrawableText> fDrawableText;
std::unique_ptr<SelectableText> fSelectableText;
SkSize fRequiredSize;
SkSize fActualSize;
SkPoint fOffset;
@ -119,7 +129,6 @@ protected:
SkSpan<DecoratedBlock> fDecorations;
TextAlign fTextAlign;
TextDirection fTextDirection;
bool fInvalidated;
};
// Text can change; supports select/copy/paste
@ -133,26 +142,20 @@ public:
bool isEmpty() { return fText.empty(); }
Position adjustedPosition(PositionType positionType, SkPoint point) const {
return fFormattedText->adjustedPosition(positionType, point - fOffset);
return fSelectableText->adjustedPosition(positionType, point - fOffset);
}
Position adjustedPosition(PositionType positionType, TextIndex textIndex) const {
return fFormattedText->adjustedPosition(positionType, textIndex);
}
//Position adjustedPosition(PositionType positionType, TextIndex textIndex) const {
// return fSelectableText->adjustedPosition(positionType, textIndex);
//}
bool recalculateBoundaries(Position& position) const {
return fFormattedText->recalculateBoundaries(position);
}
Position previousElement(Position element) const { return fSelectableText->previousPosition(element); }
Position nextElement(Position current) const { return fSelectableText->nextPosition(current); }
Position firstElement(PositionType positionType) const { return fSelectableText->firstPosition(positionType); }
Position lastElement(PositionType positionType) const { return fSelectableText->lastPosition(positionType); }
const Line* line(size_t index) const { return fFormattedText->line(index); }
Position previousElement(Position element) const { return fFormattedText->previousElement(element); }
Position nextElement(Position current) const { return fFormattedText->nextElement(current); }
Position firstElement(PositionType positionType) const { return fFormattedText->firstElement(positionType); }
Position lastElement(PositionType positionType) const { return fFormattedText->lastElement(positionType); }
bool isFirstOnTheLine(Position element) const { return fFormattedText->isFirstOnTheLine(element); }
bool isLastOnTheLine(Position element) const { return fFormattedText->isLastOnTheLine(element); }
bool isFirstOnTheLine(Position element) const { return fSelectableText->isFirstOnTheLine(element); }
bool isLastOnTheLine(Position element) const { return fSelectableText->isLastOnTheLine(element); }
void removeElement(TextRange toRemove) {
std::u16string text;

View File

@ -0,0 +1,191 @@
// Copyright 2021 Google LLC.
#ifndef Interface_DEFINED
#define Interface_DEFINED
#include <string>
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.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/include/SkShaper.h"
#include "modules/skunicode/include/SkUnicode.h"
using namespace skia::text;
namespace skia {
namespace API {
/**
* This class contains all the SKUnicode/ICU information.
*/
class UnicodeText {
public:
/** Makes calls to SkShaper and collects all the shaped data.
@param blocks a range of FontBlock elements that keep information about
fonts required to shape the text.
It's utf16 range but internally it will have to be converted
to utf8 (since all shaping operations use utf8 encoding)
@param textDirection a starting text direction value
@return an object that contains the result of shaping operations
*/
std::unique_ptr<ShapedText> shape(SkSpan<FontBlock> blocks, TextDirection textDirection);
UnicodeText(std::unique_ptr<SkUnicode> unicode, SkSpan<uint16_t> utf16);
UnicodeText(std::unique_ptr<SkUnicode> unicode, const SkString& utf8);
bool hasProperty(TextIndex index, CodeUnitFlags flag) const
bool isHardLineBreak(TextIndex index) const ;
bool isSoftLineBreak(TextIndex index) const;
bool isWhitespaces(TextRange range) const;
SkUnicode* getUnicode() const;
SkSpan<const char16_t> getText16() const;
};
class WrappedText;
/**
* This class provides all the information from SkShaper/harfbuzz in a raw format.
* It does require a single existing font for each codepoint.
*/
// Question: do we provide a visitor for ShapedText?
class ShapedText {
public:
/** Break text by lines with a given width (and possible new lines).
@param unicodeText a reference to UnicodeText that is used to query Unicode information
@param width a line width at which the text gets wrapped
@param height a text height, currently not supported
@return an object that contains the result of shaping operations (wrapping and formatting).
*/
std::unique_ptr<WrappedText> wrap(UnicodeText* unicodeText, float width, float height);
SkSpan<const LogicalRun> getLogicalRuns() const;
};
/**
* This is a helper visitor class that allows a user to process the wrapped text
* structures: lines and runs (to draw them, for instance)
*/
class Visitor {
public:
virtual ~Visitor() = default;
virtual void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) { }
virtual void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) { }
virtual void onGlyphRun(const SkFont& font,
TextRange textRange, // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds, // bounds contains the physical boundaries of the run
int trailingSpaces, // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
int glyphCount, // Just the number of glyphs
const uint16_t glyphs[],
const SkPoint positions[], // Positions relative to the line
const TextIndex clusters[]) // Text indices inside the entire text
{ }
virtual void onPlaceholder(TextRange, const SkRect& bounds) { }
};
class DrawableText;
class SelectableText;
/**
* This class provides all the information about wrapped/formatted text.
*/
class WrappedText {
public:
/** Builds a list of SkTextBlobs to draw on a canvas.
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
@param blocks a range of text indices that cause an additional run breaking to be used for styling
@return an object that contains a list of SkTextBlobs to draw on a canvas
*/
std::unique_ptr<DrawableText> prepareToDraw(UnicodeText* unicodeText, PositionType positionType, SkSpan<TextIndex> blocks) const;
/** Aggregates all the data to navigate the text (move up, down, left, right),
select some text near the cursor point, adjust all text position to word,
grapheme cluster and such.
@return an object that contains all the data for navigation
*/
std::unique_ptr<SelectableText> prepareToEdit(UnicodeText* unicodeText) const;
/** Formats a text line by line.
@param textAlign specifies a text placement on the line:
left, right, center and justified (last one currently not supported)
@param textDirection specifies a text direction that also used in formatting
*/
void format(TextAlign textAlign, TextDirection textDirection);
/** Breaks the text runs into smaller runs by given list of chunks to be used for styling.
@param unicodeText a reference to UnicodeText object
@param chunks a range of text indices that cause an additional run breaking to be used for styling
*/
void decorate(UnicodeText* unicodeText, SkSpan<TextIndex> chunks);
/** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
@param visitor a reference to Visitor object
*/
void visit(Visitor* visitor) const;
/** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
@param unicodeText a reference to UnicodeText object
@param visitor a reference to Visitor object
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
to map text blocks to glyph ranges.
@param blocks a range of text indices that cause an additional run breaking to be used for styling
*/
void visit(UnicodeText* unicodeText, Visitor* visitor, PositionType positionType, SkSpan<TextIndex> blocks) const;
};
/** This class contains all the data that allows easily paint the text on canvas.
Strictly speaking, it is not an important part of SkText API but
it presents a good example of SkText usages and simplifies testing.
*/
class DrawableText : public Visitor {
public:
std::vector<sk_sp<SkTextBlob>>& getTextBlobs();
};
struct Position {
Position(PositionType positionType, size_t lineIndex, GlyphRange glyphRange, TextRange textRange, SkRect rect);
Position(PositionType positionType);
PositionType fPositionType;
size_t fLineIndex;
GlyphRange fGlyphRange;
TextRange fTextRange;
SkRect fBoundaries;
};
struct BoxLine {
BoxLine(size_t index, TextRange text, bool hardBreak, SkRect bounds);
SkTArray<SkRect, true> fBoxGlyphs;
SkTArray<TextIndex, true> fTextByGlyph; // by glyph cluster
GlyphIndex fTextEnd;
GlyphIndex fTrailingSpacesEnd;
TextRange fTextRange;
size_t fIndex;
bool fIsHardBreak;
SkRect fBounds;
};
/** This class contains all the data that allows all navigation operations on the text:
move up/down/left/right, select some units of text and such.
*/
class SelectableText : public Visitor {
public:
SelectableText() = default;
/** Find the drawable unit (specified by positionType) closest to the screen point
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
@param point a physical coordinates on a screen to find the closest glyph
@return a position object that contains all required information
*/
Position adjustedPosition(PositionType positionType, SkPoint point) const;
Position previousPosition(Position current) const;
Position nextPosition(Position current) const;
Position firstPosition(PositionType positionType) const;
Position lastPosition(PositionType positionType) const;
Position firstInLinePosition(PositionType positionType, LineIndex lineIndex) const;
Position lastInLinePosition(PositionType positionType, LineIndex lineIndex) const;
bool isFirstOnTheLine(Position element) const;
bool isLastOnTheLine(Position element) const;
size_t countLines() const;
BoxLine getLine(size_t lineIndex) const;
};
} // namespace text
} // namespace skia
#endif // Processor_DEFINED

View File

@ -0,0 +1,40 @@
---
title: "SkText public API"
linkTitle: "SkText public API"
---
The main idea behind SkText is to design a flexible API that allows a user from different platforms to shape, manipulate and draw text.
As text formatting works in stages, SkText presents a set of text objects that keep data from each stage. That staging approach allows a user to stop at any stage and drop all the data from the previous stages.
Currently, we support the following stages:
* <u>Parsing the text</u> to extract all the unicode information that may be needed later: graphemes, words, hard/soft line breaks, whitespaces, bidi regions and so on.
Class <b>UnicodeText</b> contains the initial text in utf16 format, and the mapping of unicode information to codepoints in a form of enum bit flags.
It also has a method <b>shape</b> that creates a ShapedText object.
* <u>Shaping the text</u> into a single line of glyphs according to all given formatting information (fonts, text direction, scripts, languages and so on).
Class <b>ShapedText</b> contains the results in the form of a list of shaped blocks with all the information that comes from shaping: glyph ids, positioning, and mapping to the initial text and so on.
It also has a method <b>wrap</b> that creates a WrappedText object.
* <u>Wrapping the text</u> into a list of lines (by a given width) and formatting it on the lines (left, right, center alignment or justification).
Class <b>WrappedText</b> contains the shaped results from the previous stage only broken by lines and repositioned by lines.
It also has a method <b>prepareToDraw</b> that creates a DrawableText object, and a method <b>prepareToNavigate</b> that creates a SelectableText object.
* <u>Drawing the text</u> into a canvas. This is more of an example of how to draw the text because it only covers a rather simple case of drawing (limited decorating supported). Based on this example a user can create a custom drawing class as complex as needed.
Class <b>DrawableText</b> contains a list of SkTextBlob objects ready to be drawn on a canvas.
* <u>Navigating the text</u> by grapheme cluster (later, grapheme, glyph cluster or glyph). It has all the functionality for text editing.
Class <b>SelectableText</b> contains all the methods for that: hit test, move position (left, right, up, down, to the beginning of the text or the line, to the end of the text or the line), select an arbitrary text (aligned to a grapheme cluster) and so on.
All the objects described above can exist independently of each other, so a user can decide which ones to keep.
Lets consider few scenarios (flows):
1. A user only needs to get the unicode information.
<br>Strictly speaking, its not a text shaping operation but UnicodeText would allow it.
1. A user needs to draw the text.
<br>That requires performing the first 4 stages, but a user only needs to hold on to the DrawableText object afterwards (removing UnicodeText, ShapedText and WrappedText objects).
1. A user needs to draw and edit the text.
<br>That requires performing all the 5 stages. A user will have to hold on to DrawableText and SelectableText (removing the first 3 objects).
1. A user only needs to be wrapped text, implementing a custom drawing procedure.
<br>That requires performing the first 3 stages. A user will have to hold on to WrappedText (removing the first 2 objects).
At the moment there is no support for updating the text (which theoretically could be a less expensive operation). Any changes in the initial text will require the full set of operations.

View File

@ -1,10 +1,11 @@
// Copyright 2021 Google LLC.
#ifndef Processor_DEFINED
#define Processor_DEFINED
#ifndef Text_DEFINED
#define Text_DEFINED
#include <string>
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/TextRun.h"
#include "experimental/sktext/src/LogicalRun.h"
#include "experimental/sktext/src/VisualRun.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
@ -18,278 +19,306 @@
namespace skia {
namespace text {
class UnicodeText;
class Text {
public:
static std::unique_ptr<UnicodeText> parse(SkSpan<uint16_t> utf16);
};
// Font iterator that finds all formatting marks
// and breaks runs on them (so we can select and interpret them later)
class FormattingFontIterator final : public SkShaper::FontRunIterator {
public:
FormattingFontIterator(TextIndex textCount,
SkSpan<FontBlock> fontBlocks,
SkSpan<TextIndex> marks)
: fTextCount(textCount)
, fFontBlocks(fontBlocks)
, fFormattingMarks(marks)
, fCurrentBlock(fontBlocks.begin())
, fCurrentMark(marks.begin())
, fCurrentFontIndex(0)
, fCurrentFont(fCurrentBlock->createFont()) {}
void consume() override {
SkASSERT(fCurrentBlock < fFontBlocks.end());
SkASSERT(fCurrentMark < fFormattingMarks.end());
if (fCurrentFontIndex <= *fCurrentMark) {
if (fCurrentFontIndex == *fCurrentMark) {
++fCurrentMark;
}
++fCurrentBlock;
if (fCurrentBlock < fFontBlocks.end()) {
fCurrentFontIndex += fCurrentBlock->charCount;
fCurrentFont = fCurrentBlock->createFont();
}
} else {
++fCurrentMark;
}
}
size_t endOfCurrentRun() const override {
SkASSERT(fCurrentMark != fFormattingMarks.end() || fCurrentBlock != fFontBlocks.end());
if (fCurrentMark == fFormattingMarks.end()) {
return fCurrentFontIndex;
} else if (fCurrentBlock == fFontBlocks.end()) {
return *fCurrentMark;
} else {
return fCurrentFontIndex <= *fCurrentMark ? fCurrentFontIndex : *fCurrentMark;
}
}
bool atEnd() const override {
return (fCurrentBlock == fFontBlocks.end() || fCurrentFontIndex == fTextCount) &&
(fCurrentMark == fFormattingMarks.end() || *fCurrentMark == fTextCount);
}
const SkFont& currentFont() const override { return fCurrentFont; }
private:
TextIndex const fTextCount;
SkSpan<FontBlock> fFontBlocks;
SkSpan<TextIndex> fFormattingMarks;
FontBlock* fCurrentBlock;
TextIndex* fCurrentMark;
TextIndex fCurrentFontIndex;
SkFont fCurrentFont;
};
class ShapedText;
class UnicodeText : public SkShaper::RunHandler {
/**
* This class contains all the SKUnicode/ICU information.
*/
class UnicodeText {
public:
/** Makes calls to SkShaper and collects all the shaped data.
@param blocks a range of FontBlock elements that keep information about
fonts required to shape the text.
It's utf16 range but internally it will have to be converted
to utf8 (since all shaping operations use utf8 encoding)
@param textDirection a starting text direction value
@return an object that contains the result of shaping operations
*/
std::unique_ptr<ShapedText> shape(SkSpan<FontBlock> blocks, TextDirection textDirection);
bool hasProperty(size_t index, CodeUnitFlags flag) {
UnicodeText(std::unique_ptr<SkUnicode> unicode, SkSpan<uint16_t> utf16);
UnicodeText(std::unique_ptr<SkUnicode> unicode, const SkString& utf8);
~UnicodeText() = default;
bool hasProperty(TextIndex index, CodeUnitFlags flag) const {
return (fCodeUnitProperties[index] & flag) == flag;
}
bool isHardLineBreak(size_t index) {
bool isHardLineBreak(TextIndex index) const {
return this->hasProperty(index, CodeUnitFlags::kHardLineBreakBefore);
}
bool isSoftLineBreak(size_t index) {
bool isSoftLineBreak(TextIndex index) const {
return index != 0 && this->hasProperty(index, CodeUnitFlags::kSoftLineBreakBefore);
}
bool isWhitespaces(TextRange range) const;
SkUnicode* getUnicode() const { return fUnicode.get(); }
SkSpan<const char16_t> getText16() const { return SkSpan<const char16_t>(fText16.data(), fText16.size());}
private:
friend class Text;
UnicodeText() : fCurrentRun(nullptr) {}
void initialize(SkSpan<uint16_t> utf16);
SkTArray<CodeUnitFlags, true> fCodeUnitProperties;
std::u16string fText16;
std::unique_ptr<SkUnicode> fUnicode;
};
class WrappedText;
/**
* This class provides all the information from SkShaper/harfbuzz in a raw format.
* It does require a single existing font for each codepoint.
*/
// Question: do we provide a visitor for ShapedText?
class ShapedText : public SkShaper::RunHandler {
public:
/** Break text by lines with a given width (and possible new lines).
@param unicodeText a reference to UnicodeText that is used to query Unicode information
@param width a line width at which the text gets wrapped
@param height a text height, currently not supported
@return an object that contains the result of shaping operations (wrapping and formatting).
*/
std::unique_ptr<WrappedText> wrap(UnicodeText* unicodeText, float width, float height);
ShapedText()
: fCurrentRun(nullptr)
, fParagraphTextStart(0)
, fRunGlyphStart(0.0f)
, fTextHeight(0.0f) { }
void beginLine() override {}
void runInfo(const RunInfo&) override {}
void commitRunInfo() override {}
void commitLine() override {}
void commitRunBuffer(const RunInfo&) override;
void commitRunBuffer(const RunInfo&) override {
fCurrentRun->commit();
fLogicalRuns.emplace_back(std::move(*fCurrentRun));
fRunGlyphStart += fCurrentRun->width();
}
Buffer runBuffer(const RunInfo& info) override {
fCurrentRun = std::make_unique<TextRun>(info, fParagraphTextStart, fRunGlyphStart);
fCurrentRun = std::make_unique<LogicalRun>(info, fParagraphTextStart, fRunGlyphStart);
return fCurrentRun->newRunBuffer();
}
SkFont createFont(const FontBlock& fontBlock);
SkTArray<size_t, true> fUTF16FromUTF8;
SkTArray<size_t, true> fUTF8FromUTF16;
SkTArray<CodeUnitFlags, true> fCodeUnitProperties;
SkString fText8;
std::u16string fText16;
std::unique_ptr<SkUnicode> fUnicode;
std::unique_ptr<TextRun> fCurrentRun;
TextIndex fParagraphTextStart;
SkScalar fRunGlyphStart;
std::unique_ptr<ShapedText> fShapedText;
};
class WrappedText;
class ShapedText {
public:
std::unique_ptr<WrappedText> wrap(float width, float height, SkUnicode* unicode);
bool isClusterEdge(size_t index) const {
return (fGlyphUnitProperties[index] & GlyphUnitFlags::kGlyphClusterStart) ==
GlyphUnitFlags::kGlyphClusterStart;
}
void adjustLeft(size_t* index) const {
SkASSERT(index != nullptr);
while (*index != 0) {
if (isClusterEdge(*index)) {
return;
}
--index;
}
}
void adjustRight(size_t* index) const {
SkASSERT(index != nullptr);
while (*index < this->fGlyphUnitProperties.size()) {
if (isClusterEdge(*index)) {
return;
}
++index;
}
}
bool hasProperty(size_t index, GlyphUnitFlags flag) {
return (fGlyphUnitProperties[index] & flag) == flag;
}
bool isHardLineBreak(size_t index) {
return this->hasProperty(index, GlyphUnitFlags::kHardLineBreakBefore);
}
bool isSoftLineBreak(size_t index) {
return index != 0 && this->hasProperty(index, GlyphUnitFlags::kSoftLineBreakBefore);
}
bool isWhitespaces(TextRange range) {
if (range.leftToRight()) {
for (auto i = range.fStart; i < range.fEnd; ++i) {
if (!this->hasProperty(i, GlyphUnitFlags::kPartOfWhiteSpace)) {
return false;
}
}
} else {
for (auto i = range.fStart; i > range.fEnd; --i) {
if (!this->hasProperty(i, GlyphUnitFlags::kPartOfWhiteSpace)) {
return false;
}
}
}
return true;
}
SkSpan<const LogicalRun> getLogicalRuns() const { return SkSpan<const LogicalRun>(fLogicalRuns.begin(), fLogicalRuns.size()); }
private:
friend class UnicodeText;
ShapedText() {}
SkTArray<TextRun, false> fRuns;
SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
void addLine(WrappedText* wrappedText, SkUnicode* unicode, Stretch& stretch, Stretch& spaces, bool hardLineBreak);
SkTArray<int32_t> getVisualOrder(SkUnicode* unicode, RunIndex start, RunIndex end);
// This is all the results from shaping
SkTArray<LogicalRun, false> fLogicalRuns;
// Temporary values
std::unique_ptr<LogicalRun> fCurrentRun;
TextIndex fParagraphTextStart;
SkScalar fRunGlyphStart;
SkScalar fTextHeight;
};
class FormattedText;
/**
* This is a helper visitor class that allows a user to process the wrapped text
* structures: lines and runs (to draw them, for instance)
*/
class Visitor {
public:
virtual ~Visitor() = default;
virtual void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) { }
virtual void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) { }
virtual void onGlyphRun(const SkFont& font,
TextRange textRange, // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds, // bounds contains the physical boundaries of the run
int trailingSpaces, // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
int glyphCount, // Just the number of glyphs
const uint16_t glyphs[],
const SkPoint positions[], // Positions relative to the line
const TextIndex clusters[]) // Text indices inside the entire text
{ }
virtual void onPlaceholder(TextRange, const SkRect& bounds) { }
};
class DrawableText;
class SelectableText;
/**
* This class provides all the information about wrapped/formatted text.
*/
class WrappedText {
public:
sk_sp<FormattedText> format(TextAlign textAlign, TextDirection textDirection);
/** Builds a list of SkTextBlobs to draw on a canvas.
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
@param blocks a range of text indices that cause an additional run breaking to be used for styling
@return an object that contains a list of SkTextBlobs to draw on a canvas
*/
std::unique_ptr<DrawableText> prepareToDraw(UnicodeText* unicodeText, PositionType positionType, SkSpan<TextIndex> blocks) const;
/** Aggregates all the data to navigate the text (move up, down, left, right),
select some text near the cursor point, adjust all text position to word,
grapheme cluster and such.
@return an object that contains all the data for navigation
*/
std::unique_ptr<SelectableText> prepareToEdit(UnicodeText* unicodeText) const;
/** Formats a text line by line.
@param textAlign specifies a text placement on the line:
left, right, center and justified (last one currently not supported)
@param textDirection specifies a text direction that also used in formatting
*/
void format(TextAlign textAlign, TextDirection textDirection);
/** Breaks the text runs into smaller runs by given list of chunks to be used for styling.
@param unicodeText a reference to UnicodeText object
@param chunks a range of text indices that cause an additional run breaking to be used for styling
*/
void decorate(UnicodeText* unicodeText, SkSpan<TextIndex> chunks);
SkSize actualSize() const { return fActualSize; }
size_t countLines() const { return fLines.size(); }
size_t countLines() const { return fVisualLines.size(); }
/** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
@param visitor a reference to Visitor object
*/
void visit(Visitor* visitor) const;
/** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
@param unicodeText a reference to UnicodeText object
@param visitor a reference to Visitor object
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
to map text blocks to glyph ranges.
@param blocks a range of text indices that cause an additional run breaking to be used for styling
*/
void visit(UnicodeText* unicodeText, Visitor* visitor, PositionType positionType, SkSpan<TextIndex> blocks) const;
private:
friend class ShapedText;
WrappedText() : fActualSize(SkSize::MakeEmpty()) {}
void addLine(Stretch& stretch, Stretch& spaces, SkUnicode* unicode, bool hardLineBreak);
SkTArray<TextRun, false> fRuns;
SkTArray<Line, false> fLines;
SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
WrappedText() : fActualSize(SkSize::MakeEmpty()), fAligned(TextAlign::kNothing) { }
GlyphRange textToGlyphs(UnicodeText* unicodeText, PositionType positionType, RunIndex runIndex, TextRange textRange) const;
SkTArray<VisualRun, true> fVisualRuns; // Broken by lines
SkTArray<VisualLine, false> fVisualLines;
SkSize fActualSize;
TextAlign fAligned;
};
class FormattedText : public SkRefCnt {
/** This class contains all the data that allows easily paint the text on canvas.
Strictly speaking, it is not an important part of SkText API but
it presents a good example of SkText usages and simplifies testing.
*/
class DrawableText : public Visitor {
public:
SkSize actualSize() const { return fActualSize; }
DrawableText() = default;
// Operations starting from mouse/cursor require repositioning and adjusting by the screen
void onGlyphRun(const SkFont& font,
TextRange textRange,
SkRect bounds,
int trailingSpaces,
int glyphCount,
const uint16_t glyphs[],
const SkPoint positions[],
const TextIndex clusters[]) override {
SkTextBlobBuilder builder;
const auto& blobBuffer = builder.allocRunPos(font, SkToInt(glyphCount));
sk_careful_memcpy(blobBuffer.glyphs, glyphs, glyphCount * sizeof(uint16_t));
sk_careful_memcpy(blobBuffer.points(), positions, glyphCount * sizeof(SkPoint));
fTextBlobs.emplace_back(builder.make());
}
std::vector<sk_sp<SkTextBlob>>& getTextBlobs() { return fTextBlobs; }
private:
std::vector<sk_sp<SkTextBlob>> fTextBlobs;
};
struct Position {
Position(PositionType positionType, size_t lineIndex, GlyphRange glyphRange, TextRange textRange, SkRect rect)
: fPositionType(positionType)
, fLineIndex(lineIndex)
, fGlyphRange(glyphRange)
, fTextRange(textRange)
, fBoundaries(rect) { }
Position(PositionType positionType)
: Position(positionType, EMPTY_INDEX, EMPTY_RANGE, EMPTY_RANGE, SkRect::MakeEmpty()) { }
PositionType fPositionType;
size_t fLineIndex;
GlyphRange fGlyphRange;
TextRange fTextRange;
SkRect fBoundaries;
};
struct BoxLine {
BoxLine(size_t index, TextRange text, bool hardBreak, SkRect bounds)
: fTextRange(text), fIndex(index), fIsHardBreak(hardBreak), fBounds(bounds) { }
SkTArray<SkRect, true> fBoxGlyphs;
SkTArray<TextIndex, true> fTextByGlyph; // by glyph cluster
//SkTArray<GlyphRange, true> fRuns; // by glyph cluster
GlyphIndex fTextEnd;
GlyphIndex fTrailingSpacesEnd;
TextRange fTextRange;
size_t fIndex;
bool fIsHardBreak;
SkRect fBounds;
};
/** This class contains all the data that allows all navigation operations on the text:
move up/down/left/right, select some units of text and such.
*/
class SelectableText : public Visitor {
public:
SelectableText() = default;
/** Find the drawable unit (specified by positionType) closest to the screen point
@param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
@param point a physical coordinates on a screen to find the closest glyph
@return a position object that contains all required information
*/
Position adjustedPosition(PositionType positionType, SkPoint point) const;
// Operations of the text (adding, removing) require repositioning and adjusting by the text
Position adjustedPosition(PositionType positionType, TextIndex textIndex) const;
Position previousPosition(Position current) const;
Position nextPosition(Position current) const;
Position upPosition(Position current) const;
Position downPosition(Position current) const;
Position firstPosition(PositionType positionType) const;
Position lastPosition(PositionType positionType) const;
Position firstInLinePosition(PositionType positionType, LineIndex lineIndex) const;
Position lastInLinePosition(PositionType positionType, LineIndex lineIndex) const;
bool recalculateBoundaries(Position& position) const;
const TextRun* visuallyPreviousRun(size_t lineIndex, const TextRun* run) const;
const TextRun* visuallyNextRun(size_t lineIndex, const TextRun* run) const;
const TextRun* visuallyFirstRun(size_t lineIndex) const;
const TextRun* visuallyLastRun(size_t lineIndex) const;
bool isVisuallyFirst(size_t lineIndex, const TextRun* run) const;
bool isVisuallyLast(size_t lineIndex, const TextRun* run) const;
Position previousElement(Position element) const;
Position nextElement(Position current) const;
Position firstElement(PositionType positionType) const;
Position lastElement(PositionType positionType) const;
bool isFirstOnTheLine(Position element) const;
bool isLastOnTheLine(Position element) const;
size_t lineIndex(const Line* line) const { return line - fLines.data(); }
size_t countLines() const { return fLines.size(); }
const Line* line(size_t lineIndex) const {
return fLines.empty() ? nullptr : &fLines[lineIndex];
bool isFirstOnTheLine(Position element) const {
return (element.fGlyphRange.fStart == 0);
}
size_t runIndex(const TextRun* run) const {
return run == nullptr ? EMPTY_INDEX : run - fRuns.data();
bool isLastOnTheLine(Position element) const {
return (element.fGlyphRange.fEnd == fBoxLines.back().fBoxGlyphs.size());
}
bool hasProperty(size_t index, GlyphUnitFlags flag) const {
size_t countLines() const { return fBoxLines.size(); }
BoxLine getLine(size_t lineIndex) const {
SkASSERT(lineIndex < fBoxLines.size());
return fBoxLines[lineIndex];
}
bool hasProperty(TextIndex index, GlyphUnitFlags flag) const {
return (fGlyphUnitProperties[index] & flag) == flag;
}
class Visitor {
public:
virtual ~Visitor() = default;
virtual void onBeginLine(TextRange lineText) {}
virtual void onEndLine(TextRange lineText) {}
virtual void onGlyphRun(const SkFont& font,
TextRange textRange,
SkRect boundingRect,
int glyphCount,
const uint16_t glyphs[],
const SkPoint positions[],
const SkPoint offsets[]) {
SkTextBlobBuilder builder;
const auto& blobBuffer = builder.allocRunPos(font, SkToInt(glyphCount));
sk_careful_memcpy(blobBuffer.glyphs, glyphs, glyphCount * sizeof(uint16_t));
sk_careful_memcpy(blobBuffer.points(), positions, glyphCount * sizeof(SkPoint));
fTextBlobs.emplace_back(builder.make());
}
virtual void onPlaceholder(TextRange, const SkRect& bounds) {}
void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) override;
void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) override;
void buildTextBlobs(FormattedText* formattedText) {
fTextBlobs.clear();
formattedText->visit(this);
}
std::vector<sk_sp<SkTextBlob>>& getTextBlobs() { return fTextBlobs; }
private:
std::vector<sk_sp<SkTextBlob>> fTextBlobs;
};
// Visit runs as is by lines
void visit(Visitor*) const;
// Visit chunked runs
void visit(Visitor*, SkSpan<size_t> blocks) const;
void onGlyphRun(const SkFont& font,
TextRange textRange,
SkRect bounds,
int trailingSpaces,
int glyphCount,
const uint16_t glyphs[],
const SkPoint positions[],
const TextIndex clusters[]) override;
private:
void adjustTextRange(Position* position) const;
friend class WrappedText;
FormattedText() = default;
SkTArray<TextRun, false> fRuns;
SkTArray<Line, false> fLines;
Position findPosition(PositionType positionType, const BoxLine& line, SkScalar x) const;
// Just in theory a random glyph range can be represented by multiple text ranges (because of LTR/RTL)
// Currently we only support this method for a glyph, grapheme or grapheme cluster
// So it's guaranteed to be one text range
TextRange glyphsToText(Position position) const;
SkTArray<BoxLine, true> fBoxLines;
SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
SkSize fActualSize;
std::unique_ptr<Visitor> fVisitor;
};
} // namespace text
} // namespace skia
#endif // Processor_DEFINED
#endif // Text_DEFINED

View File

@ -19,6 +19,7 @@ enum class TextAlign {
kJustify,
kStart,
kEnd,
kNothing,
};
enum class TextDirection {
@ -28,6 +29,7 @@ enum class TextDirection {
// This enum lists all possible ways to query output positioning
enum class PositionType {
kRandomText,
kGraphemeCluster, // Both the edge of the glyph cluster and the text grapheme
kGlyphCluster,
kGlyph,
@ -39,26 +41,34 @@ enum class LineBreakType {
kHardLineBreakBefore,
};
enum class LogicalRunType {
kText,
kLineBreak
};
enum class CodeUnitFlags : uint8_t {
kNoCodeUnitFlag = (1 << 0),
kPartOfWhiteSpace = (1 << 1),
kGraphemeStart = (1 << 2),
kSoftLineBreakBefore = (1 << 3),
kHardLineBreakBefore = (1 << 4),
kAllCodeUnitFlags = ((1 << 5) - 1),
};
enum class GlyphUnitFlags : uint8_t {
kNoGlyphUnitFlag = (1 << 0),
kPartOfWhiteSpace = (1 << 1),
kGraphemeStart = (1 << 2),
kSoftLineBreakBefore = (1 << 3),
kHardLineBreakBefore = (1 << 4),
//kPartOfWhiteSpace = (1 << 1),
//kGraphemeStart = (1 << 2),
//kSoftLineBreakBefore = (1 << 3),
//kHardLineBreakBefore = (1 << 4),
kGlyphClusterStart = (1 << 5),
kGraphemeClusterStart = (1 << 6),
};
typedef size_t TextIndex;
typedef size_t GlyphIndex;
typedef size_t RunIndex;
typedef size_t LineIndex;
const size_t EMPTY_INDEX = std::numeric_limits<size_t>::max();
template <typename T>
@ -67,10 +77,22 @@ public:
Range() : fStart(0), fEnd(0) { }
Range(T start, T end) : fStart(start) , fEnd(end) { }
bool operator==(Range<T> other) {
return fStart == other.fStart && fEnd == other.fEnd;
}
bool leftToRight() const {
return fEnd >= fStart;
}
bool before(T index) const {
if (leftToRight()) {
return index >= fEnd;
} else {
return index >= fStart;
}
}
bool contains(T index) const {
if (leftToRight()) {
return index >= fStart && index < fEnd;
@ -79,6 +101,14 @@ public:
}
}
bool contains(Range<T> range) const {
if (leftToRight()) {
return range.fStart >= fStart && range.fEnd < fEnd;
} else {
return range.fStart < fStart && range.fEnd >= fEnd;
}
}
void normalize() {
if (!this->leftToRight()) {
std::swap(this->fStart, this->fEnd);
@ -176,6 +206,11 @@ struct FontBlock {
, charCount(count)
, chain(fontChain) { }
FontBlock() : FontBlock(0, nullptr) { }
FontBlock(FontBlock& block) {
this->type = block.type;
this->charCount = block.charCount;
this->chain = block.chain;
}
~FontBlock() { }
SkFont createFont() const {

View File

@ -153,7 +153,7 @@ protected:
result += ch;
}
result += u"\u202C";
return fUnicode->convertUtf16ToUtf8(result);
return SkUnicode::convertUtf16ToUtf8(result);
}
void onDrawContent(SkCanvas* canvas) override {

View File

@ -11,7 +11,8 @@ sktext_public = [
sktext_sources = [
"$_src/Line.cpp",
"$_src/LogicalRun.cpp",
"$_src/Paint.cpp",
"$_src/Text.cpp",
"$_src/TextRun.cpp",
"$_src/VisualRun.cpp",
]

View File

@ -1,11 +1,10 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/TextRun.h"
namespace skia {
namespace text {
Line::Line(const Stretch& stretch, const Stretch& spaces, SkSTArray<1, size_t, true> visualOrder, SkScalar verticalOffset, bool hardLineBreak)
LogicalLine::LogicalLine(const Stretch& stretch, const Stretch& spaces, SkScalar verticalOffset, bool hardLineBreak)
: fTextStart(stretch.glyphStart())
, fTextEnd(stretch.glyphEnd())
, fWhitespacesEnd (spaces.glyphEnd())
@ -15,9 +14,7 @@ Line::Line(const Stretch& stretch, const Stretch& spaces, SkSTArray<1, size_t, t
, fSpacesWidth(spaces.width())
, fHorizontalOffset(0.0f)
, fVerticalOffset(verticalOffset)
, fHardLineBreak(hardLineBreak)
, fRunsInVisualOrder(std::move(visualOrder)) {
, fHardLineBreak(hardLineBreak) {
SkASSERT(stretch.isEmpty() ||
spaces.isEmpty() ||
(stretch.glyphEnd() == spaces.glyphStart()));

View File

@ -82,7 +82,7 @@ private:
class Stretch {
public:
Stretch() : fGlyphStart(), fGlyphEnd(), fWidth(0), fTextRange(0, 0), fTextMetrics() { }
Stretch() : fGlyphStart(), fGlyphEnd(), fWidth(0), fTextRange(EMPTY_RANGE), fTextMetrics() { }
Stretch(GlyphPos glyphStart, size_t textIndex, const TextMetrics& metrics)
: fGlyphStart(glyphStart)
@ -97,14 +97,10 @@ public:
Stretch& operator=(const Stretch&) = default;
bool isEmpty() const {
if (this->fGlyphStart.isEmpty()) {
SkASSERT(this->fGlyphEnd.isEmpty());
if (fGlyphStart.isEmpty() || fGlyphEnd.isEmpty()) {
return true;
} else {
SkASSERT(!this->fGlyphEnd.isEmpty());
return false;
//return (this->fGlyphStart.runIndex() == this->fGlyphEnd.runIndex() &&
// this->fGlyphStart.glyphIndex() == this->fGlyphEnd.glyphIndex());
return fGlyphStart == fGlyphEnd;
}
}
@ -166,10 +162,10 @@ private:
TextMetrics fTextMetrics;
};
class Line {
class LogicalLine {
public:
Line(const Stretch& stretch, const Stretch& spaces, SkSTArray<1, size_t, true> visualOrder, SkScalar verticalOffset, bool hardLineBreak);
~Line() = default;
LogicalLine(const Stretch& stretch, const Stretch& spaces, SkScalar verticalOffset, bool hardLineBreak);
~LogicalLine() = default;
TextMetrics getMetrics() const { return fTextMetrics; }
GlyphPos glyphStart() const { return fTextStart; }
@ -179,44 +175,11 @@ public:
SkScalar withWithTrailingSpaces() const { return fTextWidth + fSpacesWidth; }
SkScalar horizontalOffset() const { return fHorizontalOffset; }
SkScalar verticalOffset() const { return fVerticalOffset; }
size_t runsNumber() const { return fRunsInVisualOrder.size(); }
size_t visualRun(size_t index) const { return fRunsInVisualOrder[index]; }
SkScalar height() const { return fTextMetrics.height(); }
SkScalar baseline() const { return fTextMetrics.baseline(); }
TextRange text() const { return fText; }
TextRange whitespaces() const { return fWhitespaces; }
bool isHardLineBreak() const { return fHardLineBreak; }
GlyphRange glyphRange(size_t runIndex, size_t runSize, bool includingTrailingSpaces) const {
GlyphIndex start = runIndex != this->glyphStart().runIndex() ? 0 : this->glyphStart().glyphIndex();
GlyphIndex end = runIndex != this->glyphTrailingEnd().runIndex() ? runSize : this->glyphTrailingEnd().glyphIndex();
if (!includingTrailingSpaces) {
// It's possible that the run in question consists of trailing spaces and therefore should not be count
if (this->runMayHaveTrailingSpaces(runIndex)) {
end = this->glyphEnd().runIndex() != runIndex
? start // The run entirely consists of trailing spaces
: this->glyphEnd().glyphIndex(); // The run has some trailing spaces
}
}
return GlyphRange(start, end);
}
bool runMayHaveTrailingSpaces(size_t runIndex) const {
size_t lastRunWithoutTrailingSpaces = this->glyphEnd().runIndex();
for (size_t v = fRunsInVisualOrder.size(); v > 0; --v) {
auto r = fRunsInVisualOrder[v - 1];
if (r == runIndex) {
// This run has trailing spaces (or entirely consists of them)
return true;
}
if (r == lastRunWithoutTrailingSpaces) {
return false;
}
}
SkASSERT(false);
return false;
}
private:
friend class WrappedText;
@ -232,7 +195,6 @@ private:
SkScalar fVerticalOffset;
TextMetrics fTextMetrics;
bool fHardLineBreak;
SkSTArray<1, size_t, true> fRunsInVisualOrder;
};
} // namespace text

View File

@ -0,0 +1,25 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/LogicalRun.h"
namespace skia {
namespace text {
LogicalRun::LogicalRun(const SkShaper::RunHandler::RunInfo& info, TextIndex textStart, SkScalar glyphOffset)
: fFont(info.fFont)
, fBidiLevel(info.fBidiLevel)
, fAdvance(info.fAdvance)
, fUtf8Range(info.utf8Range)
, fTextMetrics(info.fFont)
, fRunStart(textStart)
, fRunOffset(glyphOffset)
, fRunType(LogicalRunType::kText) {
fGlyphs.push_back_n(info.glyphCount);
fBounds.push_back_n(info.glyphCount);
fPositions.push_back_n(info.glyphCount + 1);
fOffsets.push_back_n(info.glyphCount);
fClusters.push_back_n(info.glyphCount + 1);
}
} // namespace text
} // namespace skia

View File

@ -0,0 +1,88 @@
// Copyright 2021 Google LLC.
#ifndef LogicalRun_DEFINED
#define LogicalRun_DEFINED
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "modules/skshaper/include/SkShaper.h"
namespace skia {
namespace text {
class LogicalRun {
public:
LogicalRun(const SkShaper::RunHandler::RunInfo& info, TextIndex textStart, SkScalar glyphOffset);
SkShaper::RunHandler::Buffer newRunBuffer() {
return {fGlyphs.data(), fPositions.data(), fOffsets.data(), fClusters.data(), {0.0f, 0.0f} };
}
void commit() {
fFont.getBounds(fGlyphs.data(), fGlyphs.size(), fBounds.data(), nullptr);
fPositions[fGlyphs.size()] = fAdvance;
fClusters[fGlyphs.size()] = this->leftToRight() ? fUtf8Range.end() : fUtf8Range.begin();
}
TextRange getTextRange() const { return fUtf16Range; }
SkScalar calculateWidth(GlyphRange glyphRange) const {
SkASSERT(glyphRange.fStart <= glyphRange.fEnd && glyphRange.fEnd < fPositions.size());
return fPositions[glyphRange.fEnd].fX - fPositions[glyphRange.fStart].fX;
}
SkScalar calculateWidth(GlyphIndex start, GlyphIndex end) const {
return calculateWidth(GlyphRange(start, end));
}
SkScalar width() const { return fAdvance.fX; }
SkScalar firstGlyphPosition() const { return fPositions[0].fX; }
bool leftToRight() const { return fBidiLevel % 2 == 0; }
uint8_t bidiLevel() const { return fBidiLevel; }
size_t size() const { return fGlyphs.size(); }
LogicalRunType getRunType() const { return fRunType; }
void setRunType(LogicalRunType runType) { fRunType = runType; }
template <typename Callback>
void forEachCluster(Callback&& callback) {
GlyphIndex glyph = 0;
for(; glyph < fClusters.size(); ++glyph) {
callback(glyph, fRunStart + fClusters[glyph]);
}
}
template <typename Callback>
void convertUtf16Range(Callback&& callback) {
this->fUtf16Range.fStart = callback(this->fUtf8Range.begin());
this->fUtf16Range.fEnd = callback(this->fUtf8Range.end());
}
// Convert indexes into utf16 and also shift them to be on the entire text scale
template <typename Callback>
void convertClusterIndexes(Callback&& callback) {
for (size_t glyph = 0; glyph < fClusters.size(); ++glyph) {
fClusters[glyph] = callback(fClusters[glyph]);
}
}
private:
friend class ShapedText;
friend class WrappedText;
SkFont fFont;
TextMetrics fTextMetrics;
LogicalRunType fRunType;
SkVector fAdvance;
SkShaper::RunHandler::Range fUtf8Range;
TextRange fUtf16Range;
TextIndex fRunStart;
SkScalar fRunOffset;
SkSTArray<128, SkGlyphID, true> fGlyphs;
SkSTArray<128, SkPoint, true> fPositions;
SkSTArray<128, SkPoint, true> fOffsets;
SkSTArray<128, uint32_t, true> fClusters;
SkSTArray<128, SkRect, true> fBounds;
uint8_t fBidiLevel;
};
} // namespace text
} // namespace skia
#endif

View File

@ -46,21 +46,19 @@ namespace text {
DecoratedBlock decoratedBlock(textSize, foreground, background);
Paint paint;
paint.paint(canvas, SkPoint::Make(x, y), formattedText.get(), SkSpan<DecoratedBlock>(&decoratedBlock, 1));
paint.paint(canvas, SkPoint::Make(x, y), nullptr, formattedText.get(), SkSpan<DecoratedBlock>(&decoratedBlock, 1));
return true;
}
void Paint::onBeginLine(TextRange lineText) { }
void Paint::onEndLine(TextRange lineText) { }
void Paint::onPlaceholder(TextRange lineText, const SkRect& bounds) { }
void Paint::onGlyphRun(const SkFont& font,
TextRange textRange,
SkRect boundingRect,
int trailingSpacesStart,
int glyphCount,
const uint16_t glyphs[],
const SkPoint positions[],
const SkPoint offsets[]) {
const SkPoint positions[],
const TextIndex clusters[]) {
DecoratedBlock decoratedBlock = findDecoratedBlock(textRange);
@ -76,15 +74,16 @@ namespace text {
fCanvas->drawTextBlob(blob, fXY.fX, fXY.fY, decoratedBlock.foregroundPaint);
}
sk_sp<FormattedText> Paint::layout(std::u16string text,
std::unique_ptr<WrappedText> Paint::layout(std::u16string text,
TextDirection textDirection, TextAlign textAlign,
SkSize reqSize,
SkSpan<FontBlock> fontBlocks) {
auto unicodeText = Text::parse(SkSpan<uint16_t>((uint16_t*)text.data(), text.size()));
auto unicode = SkUnicode::Make();
auto unicodeText = std::make_unique<UnicodeText>(std::move(unicode), SkSpan<uint16_t>((uint16_t*)text.data(), text.size()));
auto shapedText = unicodeText->shape(fontBlocks, textDirection);
auto wrappedText = shapedText->wrap(reqSize.width(), reqSize.height(), unicodeText->getUnicode());
auto formattedText = wrappedText->format(textAlign, textDirection);
return formattedText;
auto wrappedText = shapedText->wrap(unicodeText.get(), reqSize.width(), reqSize.height());
wrappedText->format(textAlign, textDirection);
return wrappedText;
}
DecoratedBlock Paint::findDecoratedBlock(TextRange textRange) {
@ -101,7 +100,7 @@ namespace text {
return DecoratedBlock(0, SkPaint(), SkPaint());
}
void Paint::paint(SkCanvas* canvas, SkPoint xy, FormattedText* formattedText, SkSpan<DecoratedBlock> decoratedBlocks) {
void Paint::paint(SkCanvas* canvas, SkPoint xy, UnicodeText* unicodeText, WrappedText* wrappedText, SkSpan<DecoratedBlock> decoratedBlocks) {
fCanvas = canvas;
fXY = xy;
fDecoratedBlocks = decoratedBlocks;
@ -114,7 +113,11 @@ namespace text {
chunks[i] = index;
}
formattedText->visit(this, SkSpan<size_t>(chunks.data(), chunks.size()));
if (chunks.size() == 1) {
wrappedText->visit( this);
} else {
wrappedText->visit(unicodeText, this, PositionType::kGraphemeCluster, SkSpan<size_t>(chunks.data(), chunks.size()));
}
}
} // namespace text

View File

@ -30,8 +30,8 @@ namespace text {
return fTypeface;
}
float size() const override { return fSize; }
sk_sp<SkTypeface> getTypeface() const { return fTypeface; }
bool empty() const { return fTypeface == nullptr; }
private:
sk_sp<SkTypeface> fTypeface;
@ -39,9 +39,9 @@ namespace text {
SkFontStyle fFontStyle;
};
class Paint : public FormattedText::Visitor {
class Paint : public Visitor {
public:
void paint(SkCanvas* canvas, SkPoint xy, FormattedText* formattedText, SkSpan<DecoratedBlock> decoratedBlocks);
void paint(SkCanvas* canvas, SkPoint xy, UnicodeText* unicodeText, WrappedText* wrappedText, SkSpan<DecoratedBlock> decoratedBlocks);
// Simplification (using default font manager, default font family and default everything possible)
static bool drawText(std::u16string text, SkCanvas* canvas, SkScalar x, SkScalar y);
static bool drawText(std::u16string text, SkCanvas* canvas, SkScalar width);
@ -57,21 +57,19 @@ namespace text {
SkSize reqSize, SkScalar x, SkScalar y);
private:
static sk_sp<FormattedText> layout(std::u16string text,
TextDirection textDirection, TextAlign textAlign,
SkSize reqSize,
SkSpan<FontBlock> fontBlocks);
static std::unique_ptr<WrappedText> layout(std::u16string text,
TextDirection textDirection, TextAlign textAlign,
SkSize reqSize,
SkSpan<FontBlock> fontBlocks);
void onBeginLine(TextRange lineText) override;
void onEndLine(TextRange) override;
void onGlyphRun(const SkFont& font,
TextRange textRange,
SkRect boundingRect,
int trailingSpacesStart,
int glyphCount,
const uint16_t glyphs[],
const SkPoint positions[],
const SkPoint offsets[]) override;
void onPlaceholder(TextRange, const SkRect& bounds) override;
const SkPoint positions[],
const TextIndex clusters[]) override;
// We guarantee that the text range will be inside one of the decorated blocks
DecoratedBlock findDecoratedBlock(TextRange textRange);

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +0,0 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/TextRun.h"
namespace skia {
namespace text {
class Processor;
TextRun::TextRun(const SkShaper::RunHandler::RunInfo& info, TextIndex textStart, SkScalar glyphOffset)
: fFont(info.fFont)
, fBidiLevel(info.fBidiLevel)
, fAdvance(info.fAdvance)
, fUtf8Range(info.utf8Range)
, fTextMetrics(info.fFont)
, fRunStart(textStart)
, fRunOffset(glyphOffset) {
fGlyphs.push_back_n(info.glyphCount);
fBounds.push_back_n(info.glyphCount);
fPositions.push_back_n(info.glyphCount + 1);
fOffsets.push_back_n(info.glyphCount);
fClusters.push_back_n(info.glyphCount + 1);
}
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(), fOffsets.data(), fClusters.data(), {0.0f, 0.0f} };
}
SkScalar TextRun::calculateWidth(GlyphRange glyphRange) const {
SkASSERT(glyphRange.fStart <= glyphRange.fEnd && glyphRange.fEnd < fPositions.size());
return fPositions[glyphRange.fEnd].fX - fPositions[glyphRange.fStart].fX;
}
GlyphIndex TextRun::findGlyph(TextIndex textIndex) const {
GlyphIndex glyphIndex = EMPTY_INDEX;
for (size_t i = 0; i < fClusters.size(); ++i) {
auto cluster = fClusters[i];
if (this->leftToRight()) {
if (cluster > textIndex) {
break;
}
} else if (cluster < textIndex) {
break;
}
glyphIndex = i;
}
return glyphIndex;
}
TextRange TextRun::getTextRange(GlyphRange glyphRange) const {
return TextRange(this->fClusters[glyphRange.fStart], this->fClusters[glyphRange.fEnd]);
}
GlyphRange TextRun::getGlyphRange(TextRange textRange) const {
return GlyphRange(getGlyphIndex(textRange.fStart), getGlyphIndex(textRange.fEnd));
}
GlyphIndex TextRun::getGlyphIndex(TextIndex textIndex) const {
return fGlyphByText[textIndex];
}
} // namespace text
} // namespace skia

View File

@ -1,156 +0,0 @@
// Copyright 2021 Google LLC.
#ifndef TextRun_DEFINED
#define TextRun_DEFINED
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "modules/skshaper/include/SkShaper.h"
namespace skia {
namespace text {
struct Position;
class TextRun {
public:
TextRun(const SkShaper::RunHandler::RunInfo& info, TextIndex textStart, SkScalar glyphOffset);
SkShaper::RunHandler::Buffer newRunBuffer();
void commit();
SkScalar calculateWidth(GlyphRange glyphRange) const;
SkScalar calculateWidth(GlyphIndex start, GlyphIndex end) const {
return calculateWidth(GlyphRange(start, end));
}
SkScalar width() const { return fAdvance.fX; }
SkScalar firstGlyphPosition() const { return fPositions[0].fX; }
bool leftToRight() const { return fBidiLevel % 2 == 0; }
uint8_t bidiLevel() const { return fBidiLevel; }
GlyphIndex findGlyph(TextIndex textIndex) const;
size_t size() const { return fGlyphs.size(); }
TextRange getTextRange(GlyphRange glyphRange) const;
GlyphRange getGlyphRange(TextRange textRange) const;
GlyphIndex getGlyphIndex(TextIndex textIndex) const;
GlyphIndex findGlyphIndexLeftOf(GlyphRange runGlyphRange, TextIndex textIndex) const {
GlyphIndex found = runGlyphRange.fStart;
for (auto i = runGlyphRange.fStart; i <= runGlyphRange.fEnd; ++i) {
auto clusterIndex = fClusters[i];
if ((this->leftToRight() && clusterIndex > textIndex) ||
(!this->leftToRight() && clusterIndex < textIndex)) {
break;
}
found = i;
}
return found;
}
template <typename Callback>
void forEachTextChunkInGlyphRange(SkSpan<TextIndex> textChunks, GlyphRange runGlyphRange, Callback&& callback) const {
TextRange runTextRange = this->getTextRange(runGlyphRange);
if (this->leftToRight()) {
TextIndex currentIndex = 0;
TextRange textRange(runTextRange.fStart, runTextRange.fStart);
for (auto currentTextChunk : textChunks) {
if (currentIndex >= runTextRange.fEnd) {
break;
}
currentIndex += currentTextChunk;
if (currentIndex < runTextRange.fStart) {
continue;
}
textRange.fStart = textRange.fEnd;
textRange.fEnd += currentTextChunk;
textRange.fEnd = std::min(runTextRange.fEnd, textRange.fEnd);
callback(textRange, getGlyphRange(textRange));
}
} else {
// TODO: Implement RTL
SkASSERT(false);
}
}
template <typename Callback>
void forEachCluster(Callback&& callback) {
for(auto& clusterIndex : fClusters) {
callback(fRunStart + clusterIndex);
}
}
// Convert indexes into utf16 and also shift them to be on the entire text scale
template <typename Callback>
void convertClusterIndexes(Callback&& callback) {
fGlyphByText.push_back_n(fUtf16Range.width() + 1);
TextIndex start = fClusters[0];
for (size_t glyph = 0; glyph < fClusters.size(); ++glyph) {
auto& clusterIndex = fClusters[glyph];
clusterIndex = callback(clusterIndex);
// Convert fGlyphByText, too
if (this->leftToRight()) {
for (auto i = start; i <= clusterIndex; ++i) {
fGlyphByText[i] = glyph;
}
} else {
for (auto i = clusterIndex; i >= start; --i) {
fGlyphByText[i] = glyph;
}
}
start = clusterIndex + 1;
}
}
// Convert indexes into utf16 and also shift them to be on the entire text scale
template <typename Callback>
void convertUtf16Range(Callback&& callback) {
this->fUtf16Range.fStart = callback(this->fUtf8Range.begin());
this->fUtf16Range.fEnd = callback(this->fUtf8Range.end());
}
private:
friend class ShapedText;
friend class FormattedText;
SkFont fFont;
SkVector fAdvance;
SkShaper::RunHandler::Range fUtf8Range;
TextRange fUtf16Range;
TextIndex fRunStart;
SkScalar fRunOffset;
SkSTArray<128, SkGlyphID, true> fGlyphs;
SkSTArray<128, SkPoint, true> fPositions;
SkSTArray<128, SkPoint, true> fOffsets;
SkSTArray<128, uint32_t, true> fClusters;
SkSTArray<128, SkRect, true> fBounds;
SkSTArray<128, GlyphIndex> fGlyphByText; // Sort of the opposite of fClusters
TextMetrics fTextMetrics;
uint8_t fBidiLevel;
size_t sizeWithoutTrailingSpaces() const;
};
// This is a input/output range within one run
class Line;
struct Position {
Position(PositionType positionType, size_t lineIndex, const TextRun* run, GlyphRange glyphRange, TextRange textRange, SkRect rect)
: fPositionType(positionType)
, fLineIndex(lineIndex)
, fRun(run)
, fGlyphRange(glyphRange)
, fTextRange(textRange)
, fBoundaries(rect) { }
Position(PositionType positionType)
: Position(positionType, EMPTY_INDEX, nullptr, EMPTY_RANGE, EMPTY_RANGE, SkRect::MakeEmpty()) { }
PositionType fPositionType;
size_t fLineIndex;
const TextRun* fRun;
GlyphRange fGlyphRange;
TextRange fTextRange;
SkRect fBoundaries;
};
} // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,9 @@
// Copyright 2021 Google LLC.
#include "experimental/sktext/src/VisualRun.h"
namespace skia {
namespace text {
} // namespace text
} // namespace skia

View File

@ -0,0 +1,143 @@
// Copyright 2021 Google LLC.
#ifndef VisualRun_DEFINED
#define VisualRun_DEFINED
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "modules/skshaper/include/SkShaper.h"
namespace skia {
namespace text {
class VisualRun {
public:
VisualRun(TextRange textRange, GlyphIndex trailingSpacesStart, const TextMetrics& metrics, SkScalar runOffsetInLine,
SkSpan<SkPoint> positions, SkSpan<SkGlyphID> glyphs, SkSpan<uint32_t> clusters)
: fTextMetrics(metrics)
, fUtf16Range(textRange)
, fTrailingSpacesStart(trailingSpacesStart) {
fPositions.reserve_back(positions.size());
runOffsetInLine -= positions[0].fX;
for (auto& pos : positions) {
fPositions.emplace_back(pos + SkPoint::Make(runOffsetInLine, 0));
}
fGlyphs.reserve_back(glyphs.size());
for (auto glyph : glyphs) {
fGlyphs.emplace_back(glyph);
}
fClusters.reserve_back(clusters.size());
for (auto cluster : clusters) {
fClusters.emplace_back(SkToU16(cluster));
}
fAdvance.fX = calculateWidth(0, glyphs.size());
fAdvance.fY = metrics.height();
}
SkScalar calculateWidth(GlyphRange glyphRange) const {
SkASSERT(glyphRange.fStart <= glyphRange.fEnd && glyphRange.fEnd < fPositions.size());
return fPositions[glyphRange.fEnd].fX - fPositions[glyphRange.fStart].fX;
}
SkScalar calculateWidth(GlyphIndex start, GlyphIndex end) const {
return calculateWidth(GlyphRange(start, end));
}
SkScalar width() const { return fAdvance.fX; }
SkScalar firstGlyphPosition() const { return fPositions[0].fX; }
bool leftToRight() const { return fBidiLevel % 2 == 0; }
uint8_t bidiLevel() const { return fBidiLevel; }
size_t size() const { return fGlyphs.size(); }
TextMetrics textMetrics() const { return fTextMetrics; }
GlyphIndex trailingSpacesStart() const { return fTrailingSpacesStart; }
TextRange textRange() const { return fUtf16Range; }
template <typename Callback>
void forEachTextChunkInGlyphRange(SkSpan<TextIndex> textChunks, Callback&& callback) const {
if (this->leftToRight()) {
TextIndex currentIndex = 0;
TextRange textRange(fUtf16Range.fStart, fUtf16Range.fStart);
for (auto currentTextChunk : textChunks) {
if (currentIndex >= fUtf16Range.fEnd) {
break;
}
currentIndex += currentTextChunk;
if (currentIndex < fUtf16Range.fStart) {
continue;
}
textRange.fStart = textRange.fEnd;
textRange.fEnd += currentTextChunk;
textRange.fEnd = std::min(fUtf16Range.fEnd, textRange.fEnd);
callback(textRange);
}
} else {
// TODO: Implement RTL
SkASSERT(false);
}
}
private:
friend class WrappedText;
SkFont fFont;
TextMetrics fTextMetrics;
SkVector fAdvance;
SkShaper::RunHandler::Range fUtf8Range;
TextRange fUtf16Range;
TextIndex fRunStart;
SkScalar fRunOffset;
SkSTArray<128, SkGlyphID, true> fGlyphs;
SkSTArray<128, SkPoint, true> fPositions;
SkSTArray<128, TextIndex, true> fClusters;
GlyphIndex fTrailingSpacesStart;
uint8_t fBidiLevel;
};
class VisualLine {
public:
VisualLine(TextRange text, bool hardLineBreak, SkScalar verticalOffset, SkSpan<VisualRun> runs)
: fText(text)
, fRuns(runs)
, fTrailingSpaces(0, 0)
, fOffset(SkPoint::Make(0, verticalOffset))
, fActualWidth(0.0f)
, fTextMetrics()
, fIsHardBreak(hardLineBreak)
, fGlyphCount(0ul) {
// Calculate all the info
for (auto& run : fRuns) {
fTextMetrics.merge(run.textMetrics());
fActualWidth += run.width(); // What about trailing spaces?
if (run.trailingSpacesStart() == 0) {
// The entire run is trailing spaces, do not move the counter
} else {
// We can reset the trailing spaces counter
fTrailingSpaces.fStart = fTrailingSpaces.fEnd + run.trailingSpacesStart();
}
fTrailingSpaces.fEnd += run.size();
}
}
SkScalar baseline() const { return fTextMetrics.baseline(); }
TextRange text() const { return fText; }
GlyphRange trailingSpaces() const { return fTrailingSpaces; }
bool isHardBreak() const { return fIsHardBreak; }
size_t glyphCount() const { return fGlyphCount; }
bool isFirst(VisualRun* run) { return &fRuns.front() == run; }
bool isLast(VisualRun* run) { return &fRuns.back() == run; }
private:
friend class WrappedText;
friend class VisualRun;
TextRange fText;
SkSpan<VisualRun> fRuns;
GlyphRange fTrailingSpaces; // This is a zero-based index across the line
SkPoint fOffset; // For left/right/center formatting and for height
SkScalar fActualWidth;
TextMetrics fTextMetrics;
bool fIsHardBreak;
size_t fGlyphCount;
};
}; // namespace text
} // namespace skia
#endif

View File

@ -0,0 +1,318 @@
// Copyright 2021 Google LLC.
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkImageEncoder.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSpan.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkTypeface.h"
#include "include/core/SkTypes.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/src/Paint.h"
#include <string.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct GrContextOptions;
#define VeryLongCanvasWidth 1000000
#define TestCanvasWidth 1000
#define TestCanvasHeight 600
#if !defined(SK_BUILD_FOR_UNIX)
#define DEF_TEST(name, reporter) \
static void test_##name(skiatest::Reporter* reporter, const GrContextOptions&); \
skiatest::TestRegistry name##TestRegistry(skiatest::Test(#name, false, test_##name)); \
void test_##name(skiatest::Reporter* reporter, const GrContextOptions&) { /* SkDebugf("Disabled:"#name "\n"); */ } \
void disabled_##name(skiatest::Reporter* reporter, const GrContextOptions&)
#endif
using namespace skia::text;
namespace {
bool operator==(SkSpan<const char16_t> a, SkSpan<const char16_t> b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
}
struct TestLine {
size_t index;
TextRange lineText;
bool hardBreak;
SkRect bounds;
GlyphRange trailingSpaces;
Range<RunIndex> runRange;
size_t glyphCount;
};
struct TestRun {
const SkFont& font;
TextRange textRange; // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds; // bounds contains the physical boundaries of the run
int trailingSpaces; // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
SkSpan<const uint16_t> glyphs;
SkSpan<const SkPoint> positions;
SkSpan<const TextIndex> clusters;
};
class TestVisitor : public Visitor {
public:
void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) override {
SkASSERT(fTestLines.size() == index);
fTestLines.push_back({ index, lineText, hardBreak, bounds, EMPTY_RANGE, Range<RunIndex>(fTestRuns.size(), fTestRuns.size()) });
}
void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) override {
SkASSERT(fTestLines.size() == index + 1);
fTestLines.back().trailingSpaces = trailingSpaces;
fTestLines.back().runRange.fEnd = fTestRuns.size();
fTestLines.back().glyphCount = glyphCount;
}
void onGlyphRun(const SkFont& font,
TextRange textRange, // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds, // bounds contains the physical boundaries of the run
int trailingSpaces, // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
int glyphCount, // Just the number of glyphs
const uint16_t glyphs[],
const SkPoint positions[], // Positions relative to the line
const TextIndex clusters[]) override
{
fTestRuns.push_back({font, textRange, bounds, trailingSpaces,
SkSpan<const uint16_t>(&glyphs[0], glyphCount),
SkSpan<const SkPoint>(&positions[0], glyphCount + 1),
SkSpan<const TextIndex>(&clusters[0], glyphCount + 1),
});
}
void onPlaceholder(TextRange, const SkRect& bounds) override { }
std::vector<TestLine> fTestLines;
std::vector<TestRun> fTestRuns;
};
DEF_TEST(SkText_SelectableText_Bounds, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Leading spaces\nTrailing spaces \nLong text with collapsed spaces inside wrapped into few lines");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
auto selectableText = wrappedText->prepareToEdit(&unicodeText);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
REPORTER_ASSERT(reporter, selectableText->countLines() == 5);
for (LineIndex lineIndex = 0; lineIndex < selectableText->countLines(); ++lineIndex) {
auto& testLine = testVisitor.fTestLines[lineIndex];
auto boxLine = selectableText->getLine(lineIndex);
SkScalar left = boxLine.fBounds.fLeft;
for (auto& box : boxLine.fBoxGlyphs) {
REPORTER_ASSERT(reporter, boxLine.fBounds.contains(box) || box.isEmpty());
REPORTER_ASSERT(reporter, left <= box.fLeft);
left = box.fRight;
}
GlyphIndex trailingSpaces = boxLine.fBoxGlyphs.size() - 1;
for (RunIndex runIndex = testLine.runRange.fEnd; runIndex > testLine.runRange.fStart; --runIndex) {
auto& testRun = testVisitor.fTestRuns[runIndex - 1];
if (testRun.trailingSpaces == 0) {
trailingSpaces -= testRun.glyphs.size();
} else {
trailingSpaces -= (testRun.glyphs.size() - testRun.trailingSpaces);
break;
}
}
REPORTER_ASSERT(reporter, boxLine.fTrailingSpacesEnd == testLine.trailingSpaces.fEnd);
REPORTER_ASSERT(reporter, boxLine.fTextEnd == trailingSpaces);
REPORTER_ASSERT(reporter, boxLine.fTextRange == testLine.lineText);
REPORTER_ASSERT(reporter, boxLine.fIndex == lineIndex);
REPORTER_ASSERT(reporter, boxLine.fIsHardBreak == testLine.hardBreak);
REPORTER_ASSERT(reporter, boxLine.fBounds == testLine.bounds);
}
}
DEF_TEST(SkText_SelectableText_Navigation_FirstLast, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Leading spaces\nTrailing spaces \nLong text with collapsed spaces inside wrapped into few lines");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
auto selectableText = wrappedText->prepareToEdit(&unicodeText);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
// First position
auto firstLine = testVisitor.fTestLines.front();
auto firstRun = testVisitor.fTestRuns.front();
auto firstPosition = selectableText->firstPosition(PositionType::kGraphemeCluster);
REPORTER_ASSERT(reporter, firstPosition.fLineIndex == 0);
REPORTER_ASSERT(reporter, firstPosition.fTextRange == TextRange(0, 0));
REPORTER_ASSERT(reporter, firstPosition.fGlyphRange == GlyphRange(0, 0));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(firstPosition.fBoundaries.fLeft, 0.0f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(firstPosition.fBoundaries.fTop, 0.0f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(firstPosition.fBoundaries.width(), 0.0f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(firstPosition.fBoundaries.height(), firstLine.bounds.height()));
// Last position
auto lastLine = testVisitor.fTestLines.back();
auto lastRun = testVisitor.fTestRuns.back();
auto lastPosition = selectableText->lastPosition(PositionType::kGraphemeCluster);
REPORTER_ASSERT(reporter, lastPosition.fLineIndex == testVisitor.fTestLines.size() - 1);
REPORTER_ASSERT(reporter, lastPosition.fTextRange == TextRange(utf16.size(), utf16.size()));
REPORTER_ASSERT(reporter, lastPosition.fGlyphRange == GlyphRange(lastRun.glyphs.size(), lastRun.glyphs.size()));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lastPosition.fBoundaries.fLeft, lastRun.positions[lastRun.glyphs.size()].fX));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lastPosition.fBoundaries.fTop, lastLine.bounds.fTop));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lastPosition.fBoundaries.width(), 0.0f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(lastPosition.fBoundaries.height(), lastLine.bounds.height()));
}
DEF_TEST(SkText_SelectableText_ScanRightByGraphemeClusters, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Small Text \n");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
auto selectableText = wrappedText->prepareToEdit(&unicodeText);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
auto firstPosition = selectableText->firstPosition(PositionType::kGraphemeCluster);
auto lastPosition = selectableText->lastPosition(PositionType::kGraphemeCluster);
auto position = firstPosition;
while (!(position.fGlyphRange == lastPosition.fGlyphRange)) {
auto next = selectableText->nextPosition(position);
REPORTER_ASSERT(reporter, position.fTextRange.fEnd == next.fTextRange.fStart);
if (position.fLineIndex == next.fLineIndex - 1) {
auto line = selectableText->getLine(next.fLineIndex);
REPORTER_ASSERT(reporter, next.fGlyphRange.fStart == 0);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(next.fBoundaries.fLeft, 0.0f));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(next.fBoundaries.fTop, line.fBounds.fTop));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(next.fBoundaries.height(), line.fBounds.height()));
} else {
REPORTER_ASSERT(reporter, position.fLineIndex == next.fLineIndex);
REPORTER_ASSERT(reporter, position.fGlyphRange.fEnd == next.fGlyphRange.fStart);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.fRight, next.fBoundaries.fLeft));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.fTop, next.fBoundaries.fTop));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.height(), next.fBoundaries.height()));
}
position = next;
}
}
DEF_TEST(SkText_SelectableText_ScanLeftByGraphemeClusters, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Small Text \n");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
auto selectableText = wrappedText->prepareToEdit(&unicodeText);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
auto firstPosition = selectableText->firstPosition(PositionType::kGraphemeCluster);
auto lastPosition = selectableText->lastPosition(PositionType::kGraphemeCluster);
auto position = lastPosition;
while (!(position.fGlyphRange == firstPosition.fGlyphRange)) {
auto prev = selectableText->previousPosition(position);
REPORTER_ASSERT(reporter, position.fTextRange.fEnd == prev.fTextRange.fStart);
if (position.fLineIndex == prev.fLineIndex + 1) {
auto line = selectableText->getLine(prev.fLineIndex);
REPORTER_ASSERT(reporter, prev.fGlyphRange.fEnd == line.fBoxGlyphs.size());
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(prev.fBoundaries.fRight, line.fBounds.fRight));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(prev.fBoundaries.fTop, line.fBounds.fTop));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(prev.fBoundaries.height(), line.fBounds.height()));
} else {
REPORTER_ASSERT(reporter, position.fLineIndex == prev.fLineIndex);
REPORTER_ASSERT(reporter, position.fGlyphRange.fStart == prev.fGlyphRange.fEnd);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.fLeft, prev.fBoundaries.fRight));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.fTop, prev.fBoundaries.fTop));
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(position.fBoundaries.height(), prev.fBoundaries.height()));
}
position = prev;
}
}
DEF_TEST(SkText_SelectableText_Navigation_UpDown, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Leading spaces\nTrailing spaces \nLong text with collapsed spaces inside wrapped into few lines");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
auto selectableText = wrappedText->prepareToEdit(&unicodeText);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
// Upper position
auto position = selectableText->lastInLinePosition(PositionType::kGraphemeCluster, 0);
while (position.fLineIndex > 0) {
auto down = selectableText->downPosition(position);
REPORTER_ASSERT(reporter, position.fLineIndex + 1 == down.fLineIndex);
REPORTER_ASSERT(reporter, position.fBoundaries.centerX() >= down.fBoundaries.centerX());
position = down;
}
// Down position
position = selectableText->lastInLinePosition(PositionType::kGraphemeCluster, selectableText->countLines() - 1);
while (position.fLineIndex < selectableText->countLines() - 1) {
auto down = selectableText->downPosition(position);
REPORTER_ASSERT(reporter, position.fLineIndex - 1 == down.fLineIndex);
REPORTER_ASSERT(reporter, position.fBoundaries.centerX() >= down.fBoundaries.centerX());
position = down;
}
}

View File

@ -0,0 +1,97 @@
// Copyright 2021 Google LLC.
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkImageEncoder.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSpan.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkTypeface.h"
#include "include/core/SkTypes.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/src/Paint.h"
#include <string.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct GrContextOptions;
#define VeryLongCanvasWidth 1000000
#define TestCanvasWidth 1000
#define TestCanvasHeight 600
#if !defined(SK_BUILD_FOR_UNIX)
#define DEF_TEST(name, reporter) \
static void test_##name(skiatest::Reporter* reporter, const GrContextOptions&); \
skiatest::TestRegistry name##TestRegistry(skiatest::Test(#name, false, test_##name)); \
void test_##name(skiatest::Reporter* reporter, const GrContextOptions&) { /* SkDebugf("Disabled:"#name "\n"); */ } \
void disabled_##name(skiatest::Reporter* reporter, const GrContextOptions&)
#endif
using namespace skia::text;
namespace {
bool operator==(SkSpan<const char16_t> a, SkSpan<const char16_t> b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
}
DEF_TEST(SkText_ShapedText_LTR, reporter) {
TrivialFontChain* fontChain = new TrivialFontChain("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u"Hello world\nHello world");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), sk_ref_sp<FontChain>(fontChain));
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto logicalRuns = shapedText->getLogicalRuns();
auto newLine = utf16.find_first_of(u"\n");
REPORTER_ASSERT(reporter, logicalRuns.size() == 3);
REPORTER_ASSERT(reporter, logicalRuns[1].getRunType() == LogicalRunType::kLineBreak);
REPORTER_ASSERT(reporter, logicalRuns[1].getTextRange() == TextRange(newLine, newLine + 1));
}
DEF_TEST(SkText_ShapedText_RTL, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u"\u202EHELLO WORLD\nHELLO WORLD");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto logicalRuns = shapedText->getLogicalRuns();
auto newLine = utf16.find_first_of(u"\n");
REPORTER_ASSERT(reporter, logicalRuns.size() == 3);
REPORTER_ASSERT(reporter, logicalRuns[1].getRunType() == LogicalRunType::kLineBreak);
REPORTER_ASSERT(reporter, logicalRuns[1].getTextRange() == TextRange(newLine, newLine + 1));
}

View File

@ -0,0 +1,96 @@
// Copyright 2021 Google LLC.
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkImageEncoder.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSpan.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkTypeface.h"
#include "include/core/SkTypes.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include "experimental/sktext/include/Text.h"
#include <string.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct GrContextOptions;
#define VeryLongCanvasWidth 1000000
#define TestCanvasWidth 1000
#define TestCanvasHeight 600
#if !defined(SK_BUILD_FOR_UNIX)
#define DEF_TEST(name, reporter) \
static void test_##name(skiatest::Reporter* reporter, const GrContextOptions&); \
skiatest::TestRegistry name##TestRegistry(skiatest::Test(#name, false, test_##name)); \
void test_##name(skiatest::Reporter* reporter, const GrContextOptions&) { /* SkDebugf("Disabled:"#name "\n"); */ } \
void disabled_##name(skiatest::Reporter* reporter, const GrContextOptions&)
#endif
using namespace skia::text;
namespace {
bool operator==(SkSpan<const char16_t> a, SkSpan<const char16_t> b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
}
DEF_TEST(SkText_UnicodeText_Flags, reporter) {
REPORTER_ASSERT(reporter, true);
// 01234567890 1234567890
std::u16string utf16(u"Hello word\nHello world");
SkString utf8("Hello word\nHello world");
UnicodeText unicodeText16(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
UnicodeText unicodeText8(SkUnicode::Make(), utf8);
REPORTER_ASSERT(reporter, unicodeText16.getText16() == unicodeText8.getText16(), "UTF16 and UTF8 texts should be the same\n");
auto lineBreak = utf16.find_first_of(u"\n");
for (size_t i = 0; i < unicodeText16.getText16().size(); ++i) {
if (i == lineBreak) {
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(i, CodeUnitFlags::kHardLineBreakBefore), "Pos16 %d should point to hard line break\n", lineBreak);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(i, CodeUnitFlags::kHardLineBreakBefore), "Pos8 %d should point to hard line break\n", lineBreak);
} else {
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(i, CodeUnitFlags::kGraphemeStart), "Pos16 %d should be a grapheme start\n", i);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(i, CodeUnitFlags::kGraphemeStart), "Pos8 %d should be a grapheme start\n", i);
}
}
auto space1 = utf16.find_first_of(u" ");
auto space2 = utf16.find_last_of(u" ");
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(space1, CodeUnitFlags::kPartOfWhiteSpace), "Pos16 %d should be a part of whitespaces\n", space1);
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(space1 + 1, CodeUnitFlags::kSoftLineBreakBefore), "Pos16 %d should have soft line break before\n", space1 + 1);
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(space2, CodeUnitFlags::kPartOfWhiteSpace), "Pos16 %d should be a part of whitespaces\n", space2);
REPORTER_ASSERT(reporter, unicodeText16.hasProperty(space2 + 1, CodeUnitFlags::kSoftLineBreakBefore), "Pos16 %d should have soft line break before\n", space2 + 1);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(space1, CodeUnitFlags::kPartOfWhiteSpace), "Pos8 %d should be a part of whitespaces\n", space1);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(space1 + 1, CodeUnitFlags::kSoftLineBreakBefore), "Pos8 %d should have soft line break before\n", space1 + 1);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(space2, CodeUnitFlags::kPartOfWhiteSpace), "Pos8 %d should be a part of whitespaces\n", space2);
REPORTER_ASSERT(reporter, unicodeText8 .hasProperty(space2 + 1, CodeUnitFlags::kSoftLineBreakBefore), "Pos8 %d should have soft line break before\n", space2 + 1);
}
// TODO: Test RTL text

View File

@ -0,0 +1,160 @@
// Copyright 2021 Google LLC.
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkImageEncoder.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkScalar.h"
#include "include/core/SkSpan.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkTypeface.h"
#include "include/core/SkTypes.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/src/Paint.h"
#include <string.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct GrContextOptions;
#define VeryLongCanvasWidth 1000000
#define TestCanvasWidth 1000
#define TestCanvasHeight 600
#if !defined(SK_BUILD_FOR_UNIX)
#define DEF_TEST(name, reporter) \
static void test_##name(skiatest::Reporter* reporter, const GrContextOptions&); \
skiatest::TestRegistry name##TestRegistry(skiatest::Test(#name, false, test_##name)); \
void test_##name(skiatest::Reporter* reporter, const GrContextOptions&) { /* SkDebugf("Disabled:"#name "\n"); */ } \
void disabled_##name(skiatest::Reporter* reporter, const GrContextOptions&)
#endif
using namespace skia::text;
namespace {
bool operator==(SkSpan<const char16_t> a, SkSpan<const char16_t> b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
}
struct TestLine {
size_t index;
TextRange lineText;
bool hardBreak;
SkRect bounds;
GlyphRange trailingSpaces;
Range<RunIndex> runRange;
};
struct TestRun {
const SkFont& font;
TextRange textRange; // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds; // bounds contains the physical boundaries of the run
int trailingSpaces; // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
SkSpan<const uint16_t> glyphs;
SkSpan<const SkPoint> positions;
SkSpan<const TextIndex> clusters;
};
class TestVisitor : public Visitor {
public:
void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) override {
SkASSERT(fTestLines.size() == index);
fTestLines.push_back({ index, lineText, hardBreak, bounds, EMPTY_RANGE, Range<RunIndex>(fTestRuns.size(), fTestRuns.size()) });
}
void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) override {
SkASSERT(fTestLines.size() == index + 1);
fTestLines.back().trailingSpaces = trailingSpaces;
fTestLines.back().runRange.fEnd = fTestRuns.size();
}
void onGlyphRun(const SkFont& font,
TextRange textRange, // Currently we make sure that the run edges are the grapheme cluster edges
SkRect bounds, // bounds contains the physical boundaries of the run
int trailingSpaces, // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
int glyphCount, // Just the number of glyphs
const uint16_t glyphs[],
const SkPoint positions[], // Positions relative to the line
const TextIndex clusters[]) override
{
fTestRuns.push_back({font, textRange, bounds, trailingSpaces,
SkSpan<const uint16_t>(&glyphs[0], glyphCount),
SkSpan<const SkPoint>(&positions[0], glyphCount + 1),
SkSpan<const TextIndex>(&clusters[0], glyphCount + 1),
});
}
void onPlaceholder(TextRange, const SkRect& bounds) override { }
std::vector<TestLine> fTestLines;
std::vector<TestRun> fTestRuns;
};
DEF_TEST(SkText_WrappedText_Spaces, reporter) {
sk_sp<TrivialFontChain> fontChain = sk_make_sp<TrivialFontChain>("Roboto", 40.0f, SkFontStyle::Normal());
if (fontChain->empty()) return;
std::u16string utf16(u" Leading spaces\nTrailing spaces \nLong text with collapsed spaces inside wrapped into few lines");
UnicodeText unicodeText(SkUnicode::Make(), SkSpan<uint16_t>((uint16_t*)utf16.data(), utf16.size()));
if (!unicodeText.getUnicode()) return;
FontBlock fontBlock(utf16.size(), fontChain);
auto shapedText = unicodeText.shape(SkSpan<FontBlock>(&fontBlock, 1), TextDirection::kLtr);
auto wrappedText = shapedText->wrap(&unicodeText, 440.0f, 500.0f);
TestVisitor testVisitor;
wrappedText->visit(&testVisitor);
REPORTER_ASSERT(reporter, testVisitor.fTestLines.size() == 5);
REPORTER_ASSERT(reporter, testVisitor.fTestRuns.size() == 5);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[0].trailingSpaces.width() == 0);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[1].trailingSpaces.width() == 4);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[2].trailingSpaces.width() == 6);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[3].trailingSpaces.width() == 1);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[4].trailingSpaces.width() == 0);
auto break1 = utf16.find_first_of(u"\n");
auto break2 = utf16.find_last_of(u"\n");
REPORTER_ASSERT(reporter, testVisitor.fTestLines[0].lineText.width() == break1);
REPORTER_ASSERT(reporter, testVisitor.fTestLines[1].lineText.width() == break2 - break1 - 1);
RunIndex runIndex = 0;
SkScalar verticalOffset = 0.0f;
for (int lineIndex = 0; lineIndex < testVisitor.fTestLines.size(); ++lineIndex) {
auto& line = testVisitor.fTestLines[lineIndex];
REPORTER_ASSERT(reporter, line.runRange == Range<RunIndex>(runIndex, runIndex + 1));
REPORTER_ASSERT(reporter, line.runRange.width() == 1);
auto& run = testVisitor.fTestRuns[runIndex];
REPORTER_ASSERT(reporter, line.lineText == run.textRange);
REPORTER_ASSERT(reporter, runIndex <= 1 ? line.hardBreak : !line.hardBreak);
REPORTER_ASSERT(reporter, SkScalarNearlyEqual(verticalOffset, line.bounds.fTop));
// There is only one line that is wrapped and it has enough trailing spaces to exceed the line width
REPORTER_ASSERT(reporter, (line.index == 2 ? line.bounds.width() > 440.0f: line.bounds.width() < 440.0f));
verticalOffset = line.bounds.fBottom;
++runIndex;
}
}