blob: 56bbbfd33bde4cc4390345fa72c7beaf94872c63 [file] [log] [blame]
#include "rive/text/text.hpp"
using namespace rive;
#ifdef WITH_RIVE_TEXT
#include "rive/text_engine.hpp"
#include "rive/component_dirt.hpp"
#include "rive/text/utf.hpp"
#include "rive/text/text_style.hpp"
#include "rive/text/text_value_run.hpp"
#include "rive/text/text_modifier_group.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/artboard.hpp"
#include "rive/factory.hpp"
#include "rive/clip_result.hpp"
void GlyphItr::tryAdvanceRun()
{
while (true)
{
auto run = *m_run;
if (m_glyphIndex == m_line->endGlyphIndex(run) && run != m_line->lastRun())
{
m_run++;
m_glyphIndex = m_line->startGlyphIndex(*m_run);
}
else
{
break;
}
}
}
GlyphItr& GlyphItr::operator++()
{
auto run = *m_run;
m_glyphIndex += run->dir == TextDirection::ltr ? 1 : -1;
tryAdvanceRun();
return *this;
}
OrderedLine::OrderedLine(const Paragraph& paragraph,
const GlyphLine& line,
float lineWidth,
bool wantEllipsis,
bool isEllipsisLineLast,
GlyphRun* ellipsisRun) :
m_startGlyphIndex(line.startGlyphIndex), m_endGlyphIndex(line.endGlyphIndex)
{
std::vector<const GlyphRun*> logicalRuns;
const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
if (!wantEllipsis || !buildEllipsisRuns(logicalRuns,
paragraph,
line,
lineWidth,
isEllipsisLineLast,
ellipsisRun))
{
for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
{
logicalRuns.push_back(&glyphRuns[i]);
}
if (!logicalRuns.empty())
{
m_startLogical = logicalRuns.front();
m_endLogical = logicalRuns.back();
}
}
// Now sort the runs visually.
if (paragraph.baseDirection == TextDirection::ltr || logicalRuns.empty())
{
m_runs = std::move(logicalRuns);
}
else
{
std::vector<const GlyphRun*> visualRuns;
visualRuns.reserve(logicalRuns.size());
auto itr = logicalRuns.rbegin();
auto end = logicalRuns.rend();
const GlyphRun* first = *itr;
visualRuns.push_back(first);
size_t ltrIndex = 0;
TextDirection previousDirection = first->dir;
while (++itr != end)
{
const GlyphRun* run = *itr;
if (run->dir == TextDirection::ltr && previousDirection == run->dir)
{
visualRuns.insert(visualRuns.begin() + ltrIndex, run);
}
else
{
if (run->dir == TextDirection::ltr)
{
ltrIndex = visualRuns.size();
}
visualRuns.push_back(run);
}
previousDirection = run->dir;
}
m_runs = std::move(visualRuns);
}
}
static void appendUnicode(std::vector<rive::Unichar>& unichars, const char text[])
{
const uint8_t* ptr = (const uint8_t*)text;
while (*ptr)
{
unichars.push_back(rive::UTF::NextUTF8(&ptr));
}
}
bool OrderedLine::buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns,
const Paragraph& paragraph,
const GlyphLine& line,
float lineWidth,
bool isEllipsisLineLast,
GlyphRun* storedEllipsisRun)
{
float x = 0.0f;
const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
uint32_t startGIndex = line.startGlyphIndex;
// If it's the last line we can actually early out if the whole things fits,
// so check that first with no extra shaping.
if (isEllipsisLineLast)
{
bool fits = true;
for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
{
const GlyphRun& run = glyphRuns[i];
uint32_t endGIndex =
i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();
for (uint32_t j = startGIndex; j != endGIndex; j++)
{
x += run.advances[j];
if (x > lineWidth)
{
fits = false;
goto measured;
}
}
startGIndex = 0;
}
measured:
if (fits)
{
// It fits, just get the regular glyphs.
return false;
}
}
std::vector<Unichar> ellipsisCodePoints;
appendUnicode(ellipsisCodePoints, "...");
rcp<Font> ellipsisFont = nullptr;
float ellipsisFontSize = 0.0f;
GlyphRun ellipsisRun = {};
float ellipsisWidth = 0.0f;
bool ellipsisOverflowed = false;
startGIndex = line.startGlyphIndex;
for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
{
const GlyphRun& run = glyphRuns[i];
if (run.font != ellipsisFont && run.size != ellipsisFontSize)
{
// Track the latest we've checked (even if we discard it so we don't try
// to do this again for this ellipsis).
ellipsisFont = run.font;
ellipsisFontSize = run.size;
// Get the next shape so we can check if it fits, otherwise keep using
// the last one.
TextRun ellipsisRuns[] = {{ellipsisFont,
ellipsisFontSize,
run.lineHeight,
run.letterSpacing,
(uint32_t)ellipsisCodePoints.size()}};
auto nextEllipsisShape =
ellipsisFont->shapeText(ellipsisCodePoints, Span<TextRun>(ellipsisRuns, 1));
// Hard assumption one run and para
const Paragraph& para = nextEllipsisShape[0];
const GlyphRun& nextEllipsisRun = para.runs.front();
float nextEllipsisWidth = 0;
for (size_t j = 0; j < nextEllipsisRun.glyphs.size(); j++)
{
nextEllipsisWidth += nextEllipsisRun.advances[j];
}
if (ellipsisRun.font == nullptr || x + nextEllipsisWidth <= lineWidth)
{
// This ellipsis still fits, go ahead and use it. Otherwise stick with
// the old one.
ellipsisWidth = nextEllipsisWidth;
ellipsisRun = std::move(para.runs.front());
}
}
uint32_t endGIndex =
i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();
for (uint32_t j = startGIndex; j != endGIndex; j++)
{
float advance = run.advances[j];
if (x + advance + ellipsisWidth > lineWidth)
{
m_endGlyphIndex = j;
ellipsisOverflowed = true;
break;
}
x += advance;
}
startGIndex = 0;
logicalRuns.push_back(&run);
m_endLogical = &run;
if (ellipsisOverflowed && ellipsisRun.font != nullptr)
{
*storedEllipsisRun = std::move(ellipsisRun);
logicalRuns.push_back(storedEllipsisRun);
break;
}
}
// There was enough space for it, so let's add the ellipsis (if we didn't
// already). Note that we already checked if this is the last line and found
// that the whole text didn't fit.
if (!ellipsisOverflowed && ellipsisRun.font != nullptr)
{
*storedEllipsisRun = std::move(ellipsisRun);
logicalRuns.push_back(storedEllipsisRun);
}
m_startLogical = storedEllipsisRun == logicalRuns.front() ? nullptr : logicalRuns.front();
return true;
}
void Text::buildRenderStyles()
{
for (TextStyle* style : m_renderStyles)
{
style->rewindPath();
}
m_renderStyles.clear();
if (m_shape.size() == 0)
{
m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
return;
}
const float paragraphSpace = paragraphSpacing();
// Build up ordered runs as we go.
int paragraphIndex = 0;
float y = 0.0f;
float minY = 0.0f;
float maxWidth = 0.0f;
if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
{
y -= m_lines[0][0].baseline;
minY = y;
}
int ellipsisLine = -1;
bool isEllipsisLineLast = false;
// Find the line to put the ellipsis on (line before the one that
// overflows).
bool wantEllipsis = overflow() == TextOverflow::ellipsis && sizing() == TextSizing::fixed;
int lastLineIndex = -1;
for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
{
const Paragraph& paragraph = m_shape[paragraphIndex++];
for (const GlyphLine& line : paragraphLines)
{
const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex] -
endRun.letterSpacing;
if (width > maxWidth)
{
maxWidth = width;
}
lastLineIndex++;
if (wantEllipsis && y + line.bottom <= height())
{
ellipsisLine++;
}
}
if (!paragraphLines.empty())
{
y += paragraphLines.back().bottom;
}
y += paragraphSpace;
}
if (wantEllipsis && ellipsisLine == -1)
{
// Nothing fits, just show the first line and ellipse it.
ellipsisLine = 0;
}
isEllipsisLineLast = lastLineIndex == ellipsisLine;
int lineIndex = 0;
paragraphIndex = 0;
switch (sizing())
{
case TextSizing::autoWidth:
m_bounds = AABB(0.0f, minY, maxWidth, std::max(minY, y - paragraphSpace));
break;
case TextSizing::autoHeight:
m_bounds = AABB(0.0f, minY, width(), std::max(minY, y - paragraphSpace));
break;
case TextSizing::fixed:
m_bounds = AABB(0.0f, minY, width(), minY + height());
break;
}
// Build the clip path if we want it.
if (overflow() == TextOverflow::clipped)
{
if (m_clipRenderPath == nullptr)
{
m_clipRenderPath = artboard()->factory()->makeEmptyRenderPath();
}
else
{
m_clipRenderPath->rewind();
}
AABB bounds = localBounds();
m_clipRenderPath->addRect(bounds.minX, bounds.minY, bounds.width(), bounds.height());
}
else
{
m_clipRenderPath = nullptr;
}
y = -m_bounds.height() * originY();
if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
{
y -= m_lines[0][0].baseline;
}
paragraphIndex = 0;
bool hasModifiers = haveModifiers();
if (hasModifiers)
{
uint32_t textSize = (uint32_t)m_styledText.unichars().size();
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
modifierGroup->computeCoverage(textSize);
}
}
for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
{
const Paragraph& paragraph = m_shape[paragraphIndex++];
for (const GlyphLine& line : paragraphLines)
{
switch (overflow())
{
case TextOverflow::hidden:
if (sizing() == TextSizing::fixed && y + line.bottom > height())
{
return;
}
break;
case TextOverflow::clipped:
if (sizing() == TextSizing::fixed && y + line.top > height())
{
return;
}
break;
default:
break;
}
if (lineIndex >= m_orderedLines.size())
{
// We need to still compute this line's ordered runs.
m_orderedLines.emplace_back(OrderedLine(paragraph,
line,
width(),
ellipsisLine == lineIndex,
isEllipsisLineLast,
&m_ellipsisRun));
}
const OrderedLine& orderedLine = m_orderedLines[lineIndex];
float x = -m_bounds.width() * originX() + line.startX;
float renderY = y + line.baseline;
for (auto glyphItr : orderedLine)
{
const GlyphRun* run = std::get<0>(glyphItr);
size_t glyphIndex = std::get<1>(glyphItr);
const Font* font = run->font.get();
const Vec2D& offset = run->offsets[glyphIndex];
GlyphID glyphId = run->glyphs[glyphIndex];
float advance = run->advances[glyphIndex];
RawPath path = font->getPath(glyphId);
uint32_t textIndex = 0;
uint32_t glyphCount = 0;
if (hasModifiers)
{
textIndex = run->textIndices[glyphIndex];
glyphCount = m_glyphLookup.count(textIndex);
float centerX = advance / 2.0f;
Mat2D transform =
Mat2D::fromScaleAndTranslation(run->size, run->size, -centerX, 0.0f);
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
modifierGroup->transform(coverage, transform);
}
transform =
Mat2D::fromTranslate(centerX + x + offset.x, y + line.baseline + offset.y) *
transform;
path.transformInPlace(transform);
}
else
{
path.transformInPlace(
Mat2D(run->size, 0.0f, 0.0f, run->size, x + offset.x, renderY + offset.y));
}
x += advance;
assert(run->styleId < m_runs.size());
TextStyle* style = m_runs[run->styleId]->style();
// TextValueRun::onAddedDirty botches loading if it cannot
// resolve a style, so we're confident we have a style here.
assert(style != nullptr);
// Consider this the "local" opacity.
float opacity = 1.0f;
if (hasModifiers)
{
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
if (modifierGroup->modifiesOpacity())
{
float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
opacity = modifierGroup->computeOpacity(opacity, coverage);
}
}
}
if (style->addPath(path, opacity))
{
// This was the first path added to the style, so let's mark
// it in our draw list.
m_renderStyles.push_back(style);
style->propagateOpacity(renderOpacity());
}
}
if (lineIndex == ellipsisLine)
{
return;
}
lineIndex++;
}
if (!paragraphLines.empty())
{
y += paragraphLines.back().bottom;
}
y += paragraphSpace;
}
}
const TextStyle* Text::styleFromShaperId(uint16_t id) const
{
assert(id < m_runs.size());
return m_runs[id]->style();
}
void Text::draw(Renderer* renderer)
{
ClipResult clipResult = clip(renderer);
if (clipResult == ClipResult::noClip)
{
// We didn't clip, so make sure to save as we'll be doing some
// transformations.
renderer->save();
}
if (clipResult != ClipResult::emptyClip)
{
renderer->transform(m_WorldTransform);
if (overflow() == TextOverflow::clipped && m_clipRenderPath)
{
renderer->clipPath(m_clipRenderPath.get());
}
for (auto style : m_renderStyles)
{
style->draw(renderer);
}
}
renderer->restore();
}
void Text::addRun(TextValueRun* run) { m_runs.push_back(run); }
void Text::addModifierGroup(TextModifierGroup* group) { m_modifierGroups.push_back(group); }
void Text::markShapeDirty()
{
addDirt(ComponentDirt::Path);
for (TextModifierGroup* group : m_modifierGroups)
{
group->clearRangeMaps();
}
markWorldTransformDirty();
}
void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); }
void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); }
void Text::alignValueChanged() { markShapeDirty(); }
void Text::sizingValueChanged() { markShapeDirty(); }
void Text::overflowValueChanged()
{
if (sizing() != TextSizing::autoWidth)
{
markShapeDirty();
}
}
void Text::widthChanged()
{
if (sizing() != TextSizing::autoWidth)
{
markShapeDirty();
}
}
void Text::paragraphSpacingChanged() { markPaintDirty(); }
void Text::heightChanged()
{
if (sizing() == TextSizing::fixed)
{
markShapeDirty();
}
}
void StyledText::clear()
{
m_value.clear();
m_runs.clear();
}
bool StyledText::empty() const { return m_runs.empty(); }
void StyledText::append(rcp<Font> font,
float size,
float lineHeight,
float letterSpacing,
const std::string& text,
uint16_t styleId)
{
const uint8_t* ptr = (const uint8_t*)text.c_str();
uint32_t n = 0;
while (*ptr)
{
m_value.push_back(UTF::NextUTF8(&ptr));
n += 1;
}
m_runs.push_back({std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
}
bool Text::makeStyled(StyledText& styledText, bool withModifiers) const
{
styledText.clear();
uint16_t runIndex = 0;
for (auto valueRun : m_runs)
{
auto style = valueRun->style();
const std::string& text = valueRun->text();
if (style == nullptr || style->font() == nullptr || text.empty())
{
runIndex++;
continue;
}
styledText.append(style->font(),
style->fontSize(),
style->lineHeight(),
style->letterSpacing(),
text,
runIndex++);
}
if (withModifiers)
{
for (TextModifierGroup* group : m_modifierGroups)
{
group->applyShapeModifiers(*this, styledText);
}
}
return !styledText.empty();
}
static SimpleArray<SimpleArray<GlyphLine>> breakLines(const SimpleArray<Paragraph>& paragraphs,
float width,
TextAlign align)
{
bool autoWidth = width == -1.0f;
float paragraphWidth = width;
SimpleArray<SimpleArray<GlyphLine>> lines(paragraphs.size());
size_t paragraphIndex = 0;
for (auto& para : paragraphs)
{
lines[paragraphIndex] = GlyphLine::BreakLines(para.runs, autoWidth ? -1.0f : width);
if (autoWidth)
{
paragraphWidth = std::max(paragraphWidth,
GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs));
}
paragraphIndex++;
}
paragraphIndex = 0;
for (auto& para : paragraphs)
{
GlyphLine::ComputeLineSpacing(paragraphIndex == 0,
lines[paragraphIndex],
para.runs,
paragraphWidth,
align);
paragraphIndex++;
}
return lines;
}
bool Text::modifierRangesNeedShape() const
{
for (const TextModifierGroup* modifierGroup : m_modifierGroups)
{
if (modifierGroup->needsShape())
{
return true;
}
}
return false;
}
void Text::update(ComponentDirt value)
{
Super::update(value);
if (hasDirt(value, ComponentDirt::Path))
{
// We have modifiers that need shaping we'll need to compute the coverage
// right before we build the actual shape.
bool precomputeModifierCoverage = modifierRangesNeedShape();
if (precomputeModifierCoverage)
{
makeStyled(m_modifierStyledText, false);
auto runs = m_modifierStyledText.runs();
m_modifierShape = runs[0].font->shapeText(m_modifierStyledText.unichars(), runs);
m_modifierLines = breakLines(m_modifierShape,
sizing() == TextSizing::autoWidth ? -1.0f : width(),
(TextAlign)alignValue());
m_glyphLookup.compute(m_modifierStyledText.unichars(), m_modifierShape);
uint32_t textSize = (uint32_t)m_modifierStyledText.unichars().size();
for (TextModifierGroup* group : m_modifierGroups)
{
group->computeRangeMap(m_modifierStyledText.unichars(),
m_modifierShape,
m_modifierLines,
m_glyphLookup);
group->computeCoverage(textSize);
}
}
if (makeStyled(m_styledText))
{
auto runs = m_styledText.runs();
m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
m_lines = breakLines(m_shape,
sizing() == TextSizing::autoWidth ? -1.0f : width(),
(TextAlign)alignValue());
if (!precomputeModifierCoverage && haveModifiers())
{
m_glyphLookup.compute(m_styledText.unichars(), m_shape);
uint32_t textSize = (uint32_t)m_styledText.unichars().size();
for (TextModifierGroup* group : m_modifierGroups)
{
group->computeRangeMap(m_styledText.unichars(),
m_shape,
m_lines,
m_glyphLookup);
group->computeCoverage(textSize);
}
}
}
else
{
m_shape = SimpleArray<Paragraph>();
m_lines = SimpleArray<SimpleArray<GlyphLine>>();
m_glyphLookup.clear();
}
m_orderedLines.clear();
m_ellipsisRun = {};
// Immediately build render styles so dimensions get computed.
buildRenderStyles();
}
else if (hasDirt(value, ComponentDirt::Paint))
{
buildRenderStyles();
}
else if (hasDirt(value, ComponentDirt::RenderOpacity))
{
// Note that buildRenderStyles does this too, which is why we can get
// away doing this in the else.
for (TextStyle* style : m_renderStyles)
{
style->propagateOpacity(renderOpacity());
}
}
}
AABB Text::localBounds() const
{
float width = m_bounds.width();
float height = m_bounds.height();
return AABB::fromLTWH(m_bounds.minX - width * originX(),
m_bounds.minY - height * originY(),
width,
height);
}
Core* Text::hitTest(HitInfo*, const Mat2D&)
{
if (renderOpacity() == 0.0f)
{
return nullptr;
}
return nullptr;
}
void Text::originValueChanged()
{
markPaintDirty();
markWorldTransformDirty();
}
void Text::originXChanged()
{
markPaintDirty();
markWorldTransformDirty();
}
void Text::originYChanged()
{
markPaintDirty();
markWorldTransformDirty();
}
#else
// Text disabled.
void Text::draw(Renderer* renderer) {}
Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
void Text::addRun(TextValueRun* run) {}
void Text::addModifierGroup(TextModifierGroup* group) {}
void Text::markShapeDirty() {}
void Text::update(ComponentDirt value) {}
void Text::alignValueChanged() {}
void Text::sizingValueChanged() {}
void Text::overflowValueChanged() {}
void Text::widthChanged() {}
void Text::heightChanged() {}
void Text::markPaintDirty() {}
void Text::modifierShapeDirty() {}
bool Text::modifierRangesNeedShape() const { return false; }
const TextStyle* Text::styleFromShaperId(uint16_t id) const { return nullptr; }
void Text::paragraphSpacingChanged() {}
AABB Text::localBounds() const { return AABB(); }
void Text::originValueChanged() {}
void Text::originXChanged() {}
void Text::originYChanged() {}
#endif