blob: 8c8716f124d555fb6ca581e3ebced29116b6a60a [file]
/*
* Copyright 2022 Rive
*/
#include <rive/simple_array.hpp>
#include <catch.hpp>
#include <rive/text_engine.hpp>
#include <rive/text/font_hb.hpp>
#include "rive/text/utf.hpp"
using namespace rive;
static rive::TextRun append(std::vector<rive::Unichar>* unichars,
rive::rcp<rive::Font> font,
float size,
const char text[])
{
const uint8_t* ptr = (const uint8_t*)text;
uint32_t n = 0;
while (*ptr)
{
unichars->push_back(rive::UTF::NextUTF8(&ptr));
n += 1;
}
return {std::move(font), size, -1.0f, 0.0f, n, 0};
}
static rcp<Font> loadFont(const char* filename)
{
FILE* fp = fopen("assets/RobotoFlex.ttf", "rb");
REQUIRE(fp != nullptr);
fseek(fp, 0, SEEK_END);
const size_t length = ftell(fp);
fseek(fp, 0, SEEK_SET);
std::vector<uint8_t> bytes(length);
REQUIRE(fread(bytes.data(), 1, length, fp) == length);
fclose(fp);
return HBFont::Decode(bytes);
}
TEST_CASE("line breaker separates words", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "one two three"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 1);
const auto& run = paragraph.runs.front();
REQUIRE(run.breaks.size() == 6);
REQUIRE(run.breaks[0] == 0);
REQUIRE(run.breaks[1] == 3);
REQUIRE(run.breaks[2] == 4);
REQUIRE(run.breaks[3] == 7);
REQUIRE(run.breaks[4] == 8);
REQUIRE(run.breaks[5] == 13);
}
TEST_CASE("line breaker handles multiple runs", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
truns.push_back(append(&unichars, font, 60.0f, "ee four"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 2);
{
const auto& run = paragraph.runs.front();
REQUIRE(run.breaks.size() == 5);
REQUIRE(run.breaks[0] == 0);
REQUIRE(run.breaks[1] == 3);
REQUIRE(run.breaks[2] == 4);
REQUIRE(run.breaks[3] == 7);
REQUIRE(run.breaks[4] == 8);
}
{
const auto& run = paragraph.runs.back();
REQUIRE(run.breaks.size() == 3);
REQUIRE(run.breaks[0] == 2);
REQUIRE(run.breaks[1] == 3);
REQUIRE(run.breaks[2] == 7);
}
}
TEST_CASE("line breaker handles returns", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
truns.push_back(append(&unichars,
font,
60.0f,
reinterpret_cast<const char*>(u8"ee\u2028 four")));
auto paragraphs = font->shapeText(unichars, truns);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 2);
{
const auto& run = paragraph.runs.front();
REQUIRE(run.breaks.size() == 5);
REQUIRE(run.breaks[0] == 0);
REQUIRE(run.breaks[1] == 3);
REQUIRE(run.breaks[2] == 4);
REQUIRE(run.breaks[3] == 7);
REQUIRE(run.breaks[4] == 8);
}
{
const auto& run = paragraph.runs.back();
REQUIRE(run.breaks.size() == 5);
REQUIRE(run.breaks[0] == 2);
REQUIRE(run.breaks[1] == 2);
REQUIRE(run.breaks[2] == 2);
REQUIRE(run.breaks[3] == 4);
REQUIRE(run.breaks[4] == 8);
}
}
TEST_CASE("line breaker builds lines", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "one two three"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 1);
// at 194 everything fits in one line
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 194.0f);
REQUIRE(lines.size() == 1);
auto line = lines.back();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 0);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 13);
}
// at 191 "three" should pop to second line
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 191.0f);
REQUIRE(lines.size() == 2);
{
auto line = lines.front();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 0);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 7);
}
{
auto line = lines.back();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 8);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 13);
}
}
}
TEST_CASE("line breaker deals with extremes", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "ab"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 1);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 17.0f);
REQUIRE(lines.size() == 2);
{
auto line = lines.front();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 0);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 1);
}
{
auto line = lines.back();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 1);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 2);
}
}
// Test that it also handles 0 width.
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 0.0f);
REQUIRE(lines.size() == 2);
{
auto line = lines.front();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 0);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 1);
}
{
auto line = lines.back();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 1);
REQUIRE(line.endRunIndex == 0);
REQUIRE(line.endGlyphIndex == 2);
}
}
}
TEST_CASE("line breaker breaks return characters", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(
append(&unichars,
font,
32.0f,
reinterpret_cast<const char*>(u8"hello look\u2028here")));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 1);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
REQUIRE(lines.size() == 2);
}
}
TEST_CASE("shaper separates paragraphs", "[shaper]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars,
font,
32.0f,
reinterpret_cast<const char*>(
u8"hello look\u2028here\nsecond paragraph")));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 2);
{
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.runs.size() == 1);
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
REQUIRE(lines.size() == 2);
}
{
const auto& paragraph = paragraphs.back();
REQUIRE(paragraph.runs.size() == 1);
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
REQUIRE(lines.size() == 1);
}
}
TEST_CASE("shaper handles RTL", "[shaper]")
{
auto font = loadFont("assets/IBMPlexSansArabic-Regular.ttf");
REQUIRE(font != nullptr);
// one two⏎ three
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "لمفاتيح ABC DEF"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::rtl);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
REQUIRE(lines.size() == 1);
}
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 196.0f);
REQUIRE(lines.size() == 2);
// The second line should start with DEF as it's the first word to wrap.
const auto& line = lines.back();
const auto& run = paragraph.runs[line.startRunIndex];
auto index = run.textIndices[line.startGlyphIndex];
REQUIRE(unichars[index] == 'D');
REQUIRE(unichars[index + 1] == 'E');
REQUIRE(unichars[index + 2] == 'F');
}
}
TEST_CASE("shaper handles empty space", "[shaper]")
{
auto font = loadFont("assets/IBMPlexSansArabic-Regular.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, " "));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
REQUIRE(lines.size() == 1);
}
}
TEST_CASE("line breaker deals with empty paragraphs", "[line break]")
{
auto font = loadFont("assets/IBMPlexSansArabic-Regular.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "hi\n "));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 2);
{
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
REQUIRE(lines.size() == 1);
}
}
{
const auto& paragraph = paragraphs.back();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
REQUIRE(lines.size() == 1);
auto line = lines.front();
REQUIRE(line.startRunIndex == 0);
REQUIRE(paragraph.runs[line.startRunIndex].glyphs.size() == 1);
REQUIRE(paragraph.runs[line.startRunIndex].textIndices.size() == 1);
REQUIRE(paragraph.runs[line.startRunIndex].textIndices[0] == 3);
}
}
}
TEST_CASE("line breaker deals with space only lines", "[line break]")
{
auto font = loadFont("assets/IBMPlexSansArabic-Regular.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars,
font,
32.0f,
reinterpret_cast<const char*>(u8"hi\u2028 ")));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
{
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
REQUIRE(lines.size() == 2);
}
}
}
TEST_CASE("line breaker deals with empty lines", "[line break]")
{
auto font = loadFont("assets/IBMPlexSansArabic-Regular.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
truns.push_back(append(&unichars, font, 32.0f, "hi\n"));
auto paragraphs = font->shapeText(unichars, truns);
REQUIRE(paragraphs.size() == 1);
{
const auto& paragraph = paragraphs.front();
REQUIRE(paragraph.baseDirection() == rive::TextDirection::ltr);
{
auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
REQUIRE(lines.size() == 1);
{
auto line = lines.front();
REQUIRE(line.startRunIndex == 0);
REQUIRE(line.startGlyphIndex == 0);
REQUIRE(paragraph.runs[line.startRunIndex].glyphs.size() == 3);
REQUIRE(paragraph.runs[line.startRunIndex].textIndices.size() ==
3);
REQUIRE(paragraph.runs[line.startRunIndex].textIndices[0] == 0);
}
}
}
}
// ---------------------------------------------------------------------------
// TextWordBreak. breakWord is the default and is covered by every case above;
// these pin the two new modes and the boundaries between them.
// ---------------------------------------------------------------------------
// Shapes a single 32pt run and hands back the paragraph's runs.
static SimpleArray<Paragraph> shapeOneRun(const rcp<Font>& font,
std::vector<Unichar>* unichars,
std::vector<TextRun>* truns,
const char text[])
{
truns->push_back(append(unichars, font, 32.0f, text));
return font->shapeText(*unichars, *truns);
}
TEST_CASE("word break normal never splits a word", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "ab");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
// "line breaker deals with extremes" pins breakWord splitting this into
// two lines at these widths. normal has to keep it whole and overflow
// instead -- and, crucially, still terminate: routing this through the
// knock-to-a-new-line branch would spin forever.
for (float width : {17.0f, 1.0f, 0.0f})
{
auto lines = GlyphLine::BreakLines(runs, width, TextWordBreak::normal);
REQUIRE(lines.size() == 1);
REQUIRE(lines[0].startRunIndex == 0);
REQUIRE(lines[0].startGlyphIndex == 0);
REQUIRE(lines[0].endRunIndex == 0);
REQUIRE(lines[0].endGlyphIndex == 2);
}
}
TEST_CASE("word break normal still wraps between words", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "one two three");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
const auto& run = runs.front();
// Wide enough for "one" but not for "three", so every word lands on its
// own line and only the last one overflows.
float width = (run.xpos[3] + run.xpos[4]) / 2.0f;
auto lines = GlyphLine::BreakLines(runs, width, TextWordBreak::normal);
REQUIRE(lines.size() == 3);
REQUIRE(lines[0].startGlyphIndex == 0);
REQUIRE(lines[0].endGlyphIndex == 3);
REQUIRE(lines[1].startGlyphIndex == 4);
REQUIRE(lines[1].endGlyphIndex == 7);
// "three" is kept whole even though it doesn't fit.
REQUIRE(lines[2].startGlyphIndex == 8);
REQUIRE(lines[2].endGlyphIndex == 13);
// breakWord would chop that last word up instead.
auto broken = GlyphLine::BreakLines(runs, width, TextWordBreak::breakWord);
REQUIRE(broken.size() > lines.size());
}
TEST_CASE("word break all fills the line it is already on", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "one two three");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
const auto& run = runs.front();
// Room for "one two t" but not "one two th".
float width = (run.xpos[9] + run.xpos[10]) / 2.0f;
auto lines = GlyphLine::BreakLines(runs, width, TextWordBreak::breakAll);
REQUIRE(lines.size() >= 2);
// The first line is packed into the middle of "three" rather than ending
// after "two".
REQUIRE(lines[0].startGlyphIndex == 0);
REQUIRE(lines[0].endGlyphIndex == 9);
REQUIRE(lines[1].startGlyphIndex == 9);
// breakWord leaves "three" intact on the next line at this width.
auto broken = GlyphLine::BreakLines(runs, width, TextWordBreak::breakWord);
REQUIRE(broken[0].endGlyphIndex == 7);
}
TEST_CASE("word break all never cuts inside the space before a word",
"[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "one two three");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
const auto& run = runs.front();
// Glyph 7 is the space before "three", which starts at glyph 8. Both of
// these widths land the "last position that fits" inside that space. The
// cut has to be rejected: line 0 must end after "two" with the space
// excluded (a trailing space would inflate the width ComputeMaxWidth and
// ComputeLineSpacing derive from the line), and line 1 must start at the
// word, not at the space.
for (float width : {(run.xpos[8] + run.xpos[9]) / 2.0f,
(run.xpos[7] + run.xpos[8]) / 2.0f})
{
auto lines =
GlyphLine::BreakLines(runs, width, TextWordBreak::breakAll);
REQUIRE(lines.size() >= 2);
REQUIRE(lines[0].endGlyphIndex == 7);
REQUIRE(lines[1].startGlyphIndex == 8);
}
}
TEST_CASE("word break all matches break word for a lone word", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "ab");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
// Nothing precedes the word, so both modes take the same path -- including
// at width 0, where the minimum-one-cluster guard has to keep it moving.
for (float width : {17.0f, 1.0f, 0.0f})
{
auto all = GlyphLine::BreakLines(runs, width, TextWordBreak::breakAll);
auto word =
GlyphLine::BreakLines(runs, width, TextWordBreak::breakWord);
REQUIRE(all.size() == word.size());
for (size_t i = 0; i < all.size(); i++)
{
REQUIRE(all[i] == word[i]);
}
}
}
TEST_CASE("word break all honors word joiners", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
// U+2060 word joiner between "abc" and "def".
auto paragraphs = shapeOneRun(font,
&unichars,
&truns,
"xx abc\xE2\x81\xA0"
"def");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
const auto& run = runs.front();
// Sweep widths that all land inside the joined "abc⁠def" run. No
// line may start or end at the joiner itself.
for (uint32_t i = 3; i < run.glyphs.size(); i++)
{
auto lines = GlyphLine::BreakLines(runs,
(run.xpos[i] + run.xpos[i + 1]) / 2,
TextWordBreak::breakAll);
for (const auto& line : lines)
{
for (uint32_t joiner : run.joiners)
{
if (line.startGlyphIndex < run.textIndices.size())
{
REQUIRE(run.textIndices[line.startGlyphIndex] != joiner);
}
if (line.endGlyphIndex < run.textIndices.size())
{
REQUIRE(run.textIndices[line.endGlyphIndex] != joiner);
}
}
}
}
}
TEST_CASE("word break modes hold the line invariants", "[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
const char* samples[] = {
"one two three",
" hello world ",
"supercalifragilisticexpialidocious",
"foo-bar-baz",
"ab\xE2\x81\xA0"
"cd ef",
"auto\xC2\xAD"
"mobile",
"a",
" ",
};
const float widths[] =
{-1.f, 0.f, 1.f, 5.f, 17.f, 50.f, 97.f, 191.f, 194.f, 1000.f};
const TextWordBreak modes[] = {TextWordBreak::breakWord,
TextWordBreak::normal,
TextWordBreak::breakAll};
for (const char* sample : samples)
{
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, sample);
for (const auto& paragraph : paragraphs)
{
size_t totalGlyphs = 0;
for (const auto& run : paragraph.runs)
{
totalGlyphs += run.glyphs.size();
}
for (float width : widths)
{
for (TextWordBreak mode : modes)
{
// Terminates, and can't emit more lines than there are
// places to break.
auto lines =
GlyphLine::BreakLines(paragraph.runs, width, mode);
REQUIRE(lines.size() <= totalGlyphs + 2);
const GlyphLine* previous = nullptr;
for (const auto& line : lines)
{
// Never inverted.
REQUIRE(line.endRunIndex >= line.startRunIndex);
const auto& startRun =
paragraph.runs[line.startRunIndex];
const auto& endRun = paragraph.runs[line.endRunIndex];
REQUIRE(line.startGlyphIndex <= startRun.xpos.size());
REQUIRE(line.endGlyphIndex <= endRun.xpos.size());
float from = startRun.xpos[line.startGlyphIndex];
float to = endRun.xpos[line.endGlyphIndex];
REQUIRE(to >= from);
// Lines never overlap and never run backwards.
if (previous != nullptr)
{
REQUIRE(line.startRunIndex >=
previous->endRunIndex);
if (line.startRunIndex == previous->endRunIndex)
{
REQUIRE(line.startGlyphIndex >=
previous->endGlyphIndex);
}
}
// Everything but normal has to respect the box.
if (width >= 0.0f && mode != TextWordBreak::normal &&
line.endGlyphIndex > line.startGlyphIndex + 1)
{
REQUIRE(to - from <= width);
}
previous = &line;
}
}
}
}
}
}
TEST_CASE("word break modes agree when there is nothing to break",
"[line break]")
{
auto font = loadFont("assets/RobotoFlex.ttf");
REQUIRE(font != nullptr);
std::vector<rive::TextRun> truns;
std::vector<rive::Unichar> unichars;
auto paragraphs = shapeOneRun(font, &unichars, &truns, "one two three");
REQUIRE(paragraphs.size() == 1);
const auto& runs = paragraphs.front().runs;
// Autowidth (which is also how TextWrap::noWrap is implemented) never
// overflows, so the mode is inert.
for (float width : {-1.0f, 1000.0f})
{
auto word =
GlyphLine::BreakLines(runs, width, TextWordBreak::breakWord);
for (TextWordBreak mode :
{TextWordBreak::normal, TextWordBreak::breakAll})
{
auto other = GlyphLine::BreakLines(runs, width, mode);
REQUIRE(other.size() == word.size());
for (size_t i = 0; i < word.size(); i++)
{
REQUIRE(other[i] == word[i]);
}
}
}
}