blob: d3977bdc7cb0f3f44690c2a25ec5e267ce1f971a [file]
#include "rive/text/text.hpp"
#include "rive/layout/layout_participant.hpp"
#include "rive/importers/import_stack.hpp"
using namespace rive;
#ifdef WITH_RIVE_TEXT
#include "rive/text_engine.hpp"
#include "rive/component_dirt.hpp"
#include "rive/math/rectangles_to_contour.hpp"
#include "rive/math/transform_components.hpp"
#include "rive/text/text_style_background.hpp"
#include "rive/text/text_style_paint.hpp"
#include "rive/text/text_value_run.hpp"
#include "rive/text/text_modifier_group.hpp"
#include "rive/text/text_selection_controller.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/paint/color.hpp"
#include "rive/shapes/paint/blend_mode.hpp"
#include "rive/shapes/paint/image_sampler.hpp"
#include "rive/viewmodel/viewmodel_instance_string.hpp"
#include "rive/viewmodel/viewmodel_instance_artboard.hpp"
#include "rive/text/text_style_global.hpp"
#include "rive/text/text_style_instance.hpp"
#include "rive/text/text_style_on_demand.hpp"
#include "rive/artboard.hpp"
#include "rive/bindable_artboard.hpp"
#include "rive/file.hpp"
#include "rive/factory.hpp"
#include "rive/clip_result.hpp"
#include "rive/generated/core_registry.hpp"
#include <algorithm>
#include <cstring>
#include <limits>
#include <unordered_set>
TextValueRunProperty::TextValueRunProperty(
Core* textValueRun,
TextValueRunListener* textValueRunListener,
ViewModelInstanceValue* instanceValue,
uint16_t propertyKey,
SymbolType symbolType)
: PropertySymbolDependentSingle(
textValueRun,
textValueRunListener,
instanceValue,
propertyKey)
, m_symbolType(symbolType)
{}
void TextValueRunProperty::writeValue()
{
switch (m_symbolType)
{
case SymbolType::textContent:
if (m_instanceValue->is<ViewModelInstanceString>())
{
CoreRegistry::setString(
m_coreObject,
m_propertyKey,
m_instanceValue->as<ViewModelInstanceString>()->propertyValue());
}
break;
case SymbolType::textStyle:
case SymbolType::globalTextStyle:
// Both style symbols resolve together, so whichever fires the
// result is the same.
static_cast<TextValueRunListener*>(m_coreObjectListener)->resolveStyle();
break;
default:
break;
}
}
TextValueRunListener::TextValueRunListener(
TextValueRun* textValueRun,
rcp<ViewModelInstance> instance,
Text* text)
: CoreObjectListener(textValueRun, instance)
, m_text(text)
{
createProperties();
}
void TextValueRunListener::markDirty() { m_text->markShapeDirty(); }
void TextValueRunListener::resolveStyle()
{
auto run = textValueRun();
if (run != nullptr)
{
// Never through styleId: list runs aren't in an artboard.
run->style(m_text->resolveListRunStyle(m_instance.get()));
}
}
void TextValueRunListener::createProperties()
{
CoreObjectListener::createProperties();
createPropertyListener(SymbolType::textStyle);
createPropertyListener(SymbolType::globalTextStyle);
createPropertyListener(SymbolType::textContent);
}
TextValueRunProperty* TextValueRunListener::createSinglePropertyListener(
SymbolType symbolType)
{
uint16_t propertyKey = 0;
bool expectsString = true;
switch (symbolType)
{
case SymbolType::textStyle:
propertyKey = TextValueRunBase::styleIdPropertyKey;
break;
case SymbolType::globalTextStyle:
propertyKey = TextValueRunBase::styleIdPropertyKey;
expectsString = false;
break;
case SymbolType::textContent:
propertyKey = TextValueRunBase::textPropertyKey;
break;
default:
break;
}
auto prop = m_instance->propertyValue(symbolType);
if (prop != nullptr &&
(expectsString ? prop->is<ViewModelInstanceString>()
: prop->is<ViewModelInstanceArtboard>()))
{
auto vpl = new TextValueRunProperty(
m_core,
this,
prop,
propertyKey,
symbolType);
return vpl;
}
return nullptr;
}
void TextValueRunListener::createPropertyListener(SymbolType symbolType)
{
TextValueRunProperty* listener = nullptr;
switch (symbolType)
{
case SymbolType::textStyle:
case SymbolType::globalTextStyle:
case SymbolType::textContent:
listener = createSinglePropertyListener(symbolType);
break;
default:
break;
}
if (listener != nullptr)
{
listener->writeValue();
m_properties.push_back(listener);
}
}
Text::~Text()
{
if (m_selectionController != nullptr)
{
m_selectionController->remove(this);
}
// Listeners own their runs, which point into the on-demand styles: drop
// the runs first.
for (auto& textValueRun : m_valueRunListeners)
{
delete textValueRun;
}
m_valueRunListeners.clear();
m_onDemandStyles.clear();
}
Vec2D Text::measureLayout(
float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
return measure(Vec2D(
widthMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: width,
heightMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: height));
}
void Text::controlSize(
Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction)
{
if (m_layoutWidth != size.x || m_layoutHeight != size.y ||
m_layoutWidthScaleType != (uint8_t)widthScaleType ||
m_layoutHeightScaleType != (uint8_t)heightScaleType ||
m_layoutDirection != direction)
{
m_layoutWidth = size.x;
m_layoutHeight = size.y;
m_layoutWidthScaleType = (uint8_t)widthScaleType;
m_layoutHeightScaleType = (uint8_t)heightScaleType;
m_layoutDirection = direction;
markShapeDirty(false);
}
}
TextSizing Text::effectiveSizing() const
{
#ifdef WITH_RIVE_LAYOUT
// Layout-controlled text (a participant, or the active child of a
// participating Solo) carries independent per-axis scale types; the single
// TextSizing enum can't express e.g. hug-width + fixed-height, so map each
// combination to the closest box behavior. m_layoutWidth/Height already
// hold the resolved slot sizes, so a hug axis reads as its content size
// under `fixed`.
//
// Read the scale types off the PARTICIPANT rather than off the copy
// controlSize leaves behind. Only the resolved slot has to wait for the
// solve; the scale types are authored, and controlSize hands us the
// participant's own values verbatim. Taking the copy meant the first
// measure pass -- which runs during the solve, before controlSize -- saw
// them unset and fell back to sizing(), so a fitFontSize text was fitted
// to the width the layout was offering and only settled on the next
// advance.
if (auto* participant = layoutParticipant())
{
const auto widthScaleType = (LayoutScaleType)
participant->layoutWidthScaleType();
const auto heightScaleType = (LayoutScaleType)
participant->layoutHeightScaleType();
bool wBox = widthScaleType == LayoutScaleType::fixed ||
widthScaleType == LayoutScaleType::fill;
bool hBox = heightScaleType == LayoutScaleType::fixed ||
heightScaleType == LayoutScaleType::fill;
if (!wBox && !hBox)
{
return sizing();
}
return wBox && !hBox ? TextSizing::autoHeight : TextSizing::fixed;
}
#endif
if (m_layoutWidthScaleType == std::numeric_limits<uint8_t>::max() ||
m_layoutWidthScaleType == (uint8_t)LayoutScaleType::hug ||
m_layoutHeightScaleType == (uint8_t)LayoutScaleType::hug)
{
return sizing();
}
return TextSizing::fixed;
}
void Text::clearRenderStyles()
{
for (TextStylePaint* style : m_renderStyles)
{
style->rewindPath();
}
m_renderStyles.clear();
m_drawCommands.clear();
for (auto& entry : m_sharedGeometry)
{
entry.second->rewind();
}
for (TextStyleBackground* background : m_globalBackgrounds)
{
background->resetPath();
}
m_globalBackgrounds.clear();
// Backgrounds reset off the child list; a style whose glyphs are all
// emoji never enters m_renderStyles.
buildTextStylePaints();
for (TextStylePaint* style : m_textStylePaints)
{
if (style->background() != nullptr)
{
style->background()->resetPath();
}
}
for (TextValueRun* textValueRun : m_allRuns)
{
textValueRun->resetHitTest();
}
}
// Vertical trim: the ascent above the first line's cap- or x-height and the
// descent below the last line's baseline (or natural descent) that should be
// removed from the box. Glyph layout is unchanged. Shared by computeBoundsInfo
// (rendering) and measure (layout), so it operates on supplied lines/shape.
static void computeVerticalTrim(
const SimpleArray<SimpleArray<GlyphLine>>& lines,
const SimpleArray<Paragraph>& shape,
TextTrimTop trimTop,
TextTrimBottom trimBottom,
float& topTrim,
float& bottomTrim)
{
topTrim = 0.0f;
bottomTrim = 0.0f;
if (lines.empty() ||
(trimTop == TextTrimTop::none && trimBottom == TextTrimBottom::none))
{
return;
}
if (trimTop != TextTrimTop::none && !lines.front().empty())
{
const GlyphLine& firstLine = lines.front().front();
const Paragraph& firstParagraph = shape[0];
// Largest top edge across the runs on the first line so that no glyph
// is clipped. capHeight/xHeight are stored negative (up is -Y).
float edgePx = 0.0f;
for (uint32_t i = firstLine.startRunIndex; i <= firstLine.endRunIndex; ++i)
{
const Font::LineMetrics& metrics = firstParagraph.runs[i]
.font->lineMetrics();
float edge = trimTop == TextTrimTop::cap ? metrics.capHeight
: metrics.xHeight;
edgePx = std::max(edgePx, -edge * firstParagraph.runs[i].size);
}
topTrim = std::max(0.0f, (firstLine.baseline - edgePx) - firstLine.top);
}
// Bottom trim uses the last non-empty paragraph's last line.
if (trimBottom != TextTrimBottom::none)
{
for (size_t p = lines.size(); p-- > 0;)
{
const SimpleArray<GlyphLine>& paragraphLines = lines[p];
if (paragraphLines.empty())
{
continue;
}
const GlyphLine& lastLine = paragraphLines.back();
float descentBand = lastLine.bottom - lastLine.baseline;
if (trimBottom == TextTrimBottom::alphabetic)
{
// Box bottom at the baseline: remove the whole descent.
bottomTrim = std::max(0.0f, descentBand);
}
else
{
// Box bottom at the natural descent: keep the descenders,
// remove only any extra leading below them.
const Paragraph& lastParagraph = shape[p];
float descentPx = 0.0f;
for (uint32_t i = lastLine.startRunIndex; i <= lastLine.endRunIndex;
++i)
{
const GlyphRun& run = lastParagraph.runs[i];
descentPx = std::max(
descentPx,
run.font->lineMetrics().descent * run.size);
}
bottomTrim = std::max(0.0f, descentBand - descentPx);
}
break;
}
}
}
TextBoundsInfo Text::computeBoundsInfo()
{
const float paragraphSpace = fitParagraphSpacing();
// Build up ordered runs as we go.
int paragraphIndex = 0;
float y = 0.0f;
float minY = 0.0f;
float maxWidth = 0.0f;
float ellipsedHeight = 0;
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 && overflowAsFixed();
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];
if (width > maxWidth)
{
maxWidth = width;
}
lastLineIndex++;
if (wantEllipsis && y + line.bottom <= effectiveHeight())
{
ellipsedHeight = y + line.bottom;
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;
}
auto totalHeight = ellipsisLine > 0 ? ellipsedHeight : y;
isEllipsisLineLast = lastLineIndex == ellipsisLine;
// Vertical trim: shrink the box to the chosen cap/x-height + baseline band.
// Glyph layout is unchanged; fixed sizing keeps its authored box.
float topTrim = 0.0f;
float bottomTrim = 0.0f;
if (effectiveSizing() != TextSizing::fixed)
{
computeVerticalTrim(
m_lines,
m_shape,
verticalTrimTop(),
verticalTrimBottom(),
topTrim,
bottomTrim);
}
return {
minY,
maxWidth,
totalHeight,
ellipsisLine,
isEllipsisLineLast,
topTrim,
bottomTrim,
};
}
float Text::fitFontScale()
{
const TextSizing sizing = effectiveSizing();
// Without a fixed dimension to fit into there is nothing to search:
// autoWidth grows in both directions, so we keep the authored size.
if (sizing == TextSizing::autoWidth && !overflowAsFixed())
{
return 1.0f;
}
return fitFontScale(
effectiveWidth(),
overflowAsFixed() ? effectiveHeight()
: std::numeric_limits<float>::max());
}
float Text::fitFontScale(float boxWidth, float boxHeight)
{
// The box we are fitting into is the *trimmed* one: computeBoundsInfo and
// measure both take the trim band off the content height before the box is
// sized, so boxHeight already excludes it (it is the height the layout
// handed back, or the offer measure is fitting into). Comparing the raw
// content height against it would charge the trim band twice and shrink
// the font by it, leaving exactly the dead space the trim removed. Gated
// like computeBoundsInfo: a fixed box keeps its authored, untrimmed size.
const bool boxIsTrimmed = effectiveSizing() != TextSizing::fixed;
// Largest authored font size across runs is our maximum; we search integer
// sizes in [1, maxSize]. Scaling all runs by a single multiplier preserves
// their relative proportions while stepping the largest run by integers.
float maxSize = 0.0f;
for (TextValueRun* valueRun : m_allRuns)
{
TextStylePaint* style = valueRun->style();
if (style != nullptr && style->font() != nullptr &&
!valueRun->text().empty())
{
// No scale turns inf finite (inf * 0 is NaN). std::max drops NaN.
if (std::isinf(style->fontSize()))
{
return 1.0f;
}
maxSize = std::max(maxSize, style->fontSize());
}
}
if (maxSize <= 1.0f)
{
return 1.0f;
}
StyledText styledText;
// Shapes and lays out the text at the given integer top size and reports
// whether it fits the bounds. Text dimensions grow monotonically with size,
// so this predicate is monotonic and binary-searchable.
auto fits = [&](int topSize) -> bool {
float scale = (float)topSize / maxSize;
if (!makeStyled(styledText, true, scale))
{
// Nothing to lay out: trivially fits.
return true;
}
const auto& runs = styledText.runs();
auto shape = runs[0].font->shapeText(styledText.unichars(), runs);
auto lines = BreakLines(shape, boxWidth, align(), wrap(), wordBreak());
float maxWidth = 0.0f;
float y = 0.0f;
size_t paragraphIndex = 0;
for (const SimpleArray<GlyphLine>& paragraphLines : lines)
{
const Paragraph& paragraph = 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];
maxWidth = std::max(maxWidth, width);
}
if (!paragraphLines.empty())
{
y += paragraphLines.back().bottom;
}
y += paragraphSpacing() * scale;
}
float topTrim = 0.0f;
float bottomTrim = 0.0f;
if (boxIsTrimmed)
{
computeVerticalTrim(
lines,
shape,
verticalTrimTop(),
verticalTrimBottom(),
topTrim,
bottomTrim);
}
bool widthFits = maxWidth <= boxWidth;
bool heightFits = y - topTrim - bottomTrim <= boxHeight;
return widthFits && heightFits;
};
// Above 2^24 floats skip integers, and hi + 1 must not overflow int.
constexpr float kMaxSearchSize = (float)(1 << 24);
// Binary search for the largest integer top size in [1, floor(maxSize)]
// that fits. If nothing fits we fall back to the minimum size (1).
int lo = 1;
int hi = (int)std::min(maxSize, kMaxSearchSize);
// Most texts fit at their authored size, which the search below only
// reaches after shaping every halving on the way up. The predicate is
// monotonic, so checking the top first returns the same size.
if (fits(hi))
{
return (float)hi / maxSize;
}
hi--;
int best = 1;
while (lo <= hi)
{
int mid = lo + (hi - lo) / 2;
if (fits(mid))
{
best = mid;
lo = mid + 1;
}
else
{
hi = mid - 1;
}
}
return (float)best / maxSize;
}
LineIter Text::shouldDrawLine(
float curY,
float totalHeight,
const GlyphLine& line)
{
switch (overflow())
{
case TextOverflow::hidden:
if (overflowAsFixed())
{
switch (verticalAlign())
{
case VerticalTextAlign::top:
if (curY + line.bottom > effectiveHeight())
{
return LineIter::yOutOfBounds;
}
break;
case VerticalTextAlign::middle:
if (curY + line.top < totalHeight / 2 - effectiveHeight() / 2)
{
return LineIter::skipThisLine;
}
if (curY + line.bottom > totalHeight / 2 + effectiveHeight() / 2)
{
return LineIter::yOutOfBounds;
}
break;
case VerticalTextAlign::bottom:
if (curY + line.top < totalHeight - effectiveHeight())
{
return LineIter::skipThisLine;
}
break;
}
}
break;
case TextOverflow::clipped:
if (overflowAsFixed())
{
switch (verticalAlign())
{
case VerticalTextAlign::top:
if (curY + line.top > effectiveHeight())
{
return LineIter::yOutOfBounds;
}
break;
case VerticalTextAlign::middle:
if (curY + line.bottom < totalHeight / 2 - effectiveHeight() / 2)
{
return LineIter::skipThisLine;
}
if (curY + line.top > totalHeight / 2 + effectiveHeight() / 2)
{
return LineIter::yOutOfBounds;
}
break;
case VerticalTextAlign::bottom:
if (curY + line.bottom < totalHeight - effectiveHeight())
{
return LineIter::skipThisLine;
}
break;
}
}
break;
default:
break;
}
return LineIter::drawLine;
}
void Text::buildRenderStyles()
{
// Step 1: reset stuff. Every build lays the lines out again, so a
// paint-only rebuild must not append to the last build's lines.
clearRenderStyles();
m_orderedLines.clear();
if (m_shape.empty())
{
m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
return;
}
// Step 2: compute ellipsis information
TextBoundsInfo info = computeBoundsInfo();
float minY = info.minY;
float maxWidth = info.maxWidth;
float totalHeight = info.totalHeight;
int ellipsisLine = info.ellipsisLine;
bool isEllipsisLineLast = info.isEllipsisLineLast;
float topTrim = info.topTrim;
float bottomTrim = info.bottomTrim;
// Step 3: update modifiers
bool hasModifiers = haveModifiers();
if (hasModifiers)
{
uint32_t textSize = (uint32_t)m_styledText.unichars().size();
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
modifierGroup->computeCoverage(textSize);
modifierGroup->resetTextFollowPath();
}
}
// Step 4: update bounds. A layout-controlled axis uses the layout's size
// so the text box always matches the layout.
const float paragraphSpace = fitParagraphSpacing();
const float boxWidth = layoutBoxWidth();
const float boxHeight = layoutBoxHeight();
const float autoSizeMaxY = std::isnan(boxHeight)
? std::max(
minY,
totalHeight - paragraphSpace - topTrim -
bottomTrim)
: minY + boxHeight;
switch (effectiveSizing())
{
case TextSizing::autoWidth:
m_bounds = AABB(
0.0f,
minY,
std::isnan(boxWidth) ? maxWidth : boxWidth,
autoSizeMaxY);
break;
case TextSizing::autoHeight:
m_bounds = AABB(0.0f, minY, effectiveWidth(), autoSizeMaxY);
break;
case TextSizing::fixed:
m_bounds = AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight());
break;
}
auto verticalAlignOffset = 0.0f;
switch (verticalAlign())
{
case VerticalTextAlign::middle:
verticalAlignOffset = (totalHeight - m_bounds.height()) / 2;
break;
case VerticalTextAlign::bottom:
verticalAlignOffset = totalHeight - m_bounds.height();
break;
default:
break;
}
// Step 5: Update clip information (if we want it)
if (overflow() == TextOverflow::clipped)
{
m_clipRect.rewind();
AABB bounds = localBounds();
float minX = bounds.minX + bounds.width() * originX();
float minY = bounds.minY + bounds.height() * originY() +
verticalAlignOffset;
m_clipRect.addRect(
AABB(minX, minY, minX + bounds.width(), minY + bounds.height()));
}
// Step 6: add the glyphs to render paths. Shift content up by topTrim so
// the first line's cap-height line aligns with the (trimmed) box top.
float curY = minY - topTrim;
int lineIndex = 0;
int paragraphIndex = 0;
float minX = std::numeric_limits<float>::max();
for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
{
const Paragraph& paragraph = m_shape[paragraphIndex++];
int lineIndexInParagraph = 0;
for (const GlyphLine& line : paragraphLines)
{
LineIter lineIter = shouldDrawLine(curY, totalHeight, line);
if (lineIter == LineIter::yOutOfBounds)
{
goto skipLines;
}
else if (lineIter == LineIter::skipThisLine)
{
lineIndexInParagraph++;
lineIndex++;
continue;
}
float renderY = curY + line.baseline;
// We need to compute this line's ordered runs.
m_orderedLines.emplace_back(OrderedLine(
paragraph,
line,
effectiveWidth(),
align(),
ellipsisLine == lineIndex,
isEllipsisLineLast,
&m_ellipsisRun,
renderY));
float curX = m_orderedLines.back().startX();
minX = std::min(curX, minX);
for (auto glyphItr : m_orderedLines.back())
{
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];
// Step 6.1: translate to the glyph's origin and scale.
Vec2D curPos(curX, renderY);
float centerX = advance / 2.0f;
TransformComponents tc;
tc.scaleX(run->size);
tc.scaleY(run->size);
tc.x(-centerX);
Mat2D pathTransform = Mat2D::compose(tc);
// Step 6.2: apply modifiers on a font-sized glyph
float opacity = 1.0f;
if (hasModifiers)
{
uint32_t textIndex = run->textIndices[glyphIndex];
uint32_t glyphCount = m_glyphLookup.count(textIndex);
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
float coverage = modifierGroup->glyphCoverage(
textIndex,
glyphCount);
TransformGlyphArg arg = {
curPos,
centerX,
lineIndexInParagraph,
paragraphLines,
};
modifierGroup->transform(coverage, pathTransform, arg);
if (modifierGroup->modifiesOpacity())
{
opacity = modifierGroup->computeOpacity(opacity, coverage);
}
}
}
// Step 6.3: translate back to center with offset
pathTransform = Mat2D::fromTranslate(
curPos.x + centerX + offset.x,
curPos.y + offset.y) *
pathTransform;
assert(run->styleId < m_allRuns.size());
TextValueRun* textValueRun = m_allRuns[run->styleId];
TextStylePaint* style = textValueRun->style();
assert(style != nullptr);
// Check for color glyph (emoji) -- draw individually
// with per-layer colors instead of accumulating.
if (font->isColorGlyph(glyphId))
{
TextDrawCommand cmd;
cmd.type = TextDrawCommand::kColorGlyph;
cmd.colorGlyph = {
run->font,
glyphId,
pathTransform,
style->foregroundColor(),
opacity};
m_drawCommands.push_back(std::move(cmd));
}
else
{
// Placed as it's added: the font's own outline, not a copy.
const RawPath& path = font->glyphPath(glyphId);
if (style->isShared())
{
// A paint other Texts draw too: keep this Text's
// glyphs here, and leave the paint untouched.
TextStyleGeometry& geometry = sharedGeometry(style);
if (geometry.addPath(path, opacity, &pathTransform))
{
TextDrawCommand cmd;
cmd.type = TextDrawCommand::kStylePath;
cmd.style = style;
cmd.geometry = &geometry;
m_drawCommands.push_back(std::move(cmd));
}
}
else if (style->addPath(path, opacity, &pathTransform))
{
// 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());
TextDrawCommand cmd;
cmd.type = TextDrawCommand::kStylePath;
cmd.style = style;
m_drawCommands.push_back(std::move(cmd));
}
}
// Bounds of the glyph
if (textValueRun->isHitTarget() || style->background() != nullptr)
{
AABB glyphBounds(
curX,
curY + line.top,
curX + advance,
curY + line.bottom);
if (textValueRun->isHitTarget())
{
textValueRun->addHitRect(glyphBounds);
}
if (style->background() != nullptr)
{
TextStyleBackground* background = style->background();
// A global style's background isn't one of this
// Text's children: remember it to reset, update and
// draw it.
if (style->parent() != this &&
std::find(
m_globalBackgrounds.begin(),
m_globalBackgrounds.end(),
background) == m_globalBackgrounds.end())
{
m_globalBackgrounds.push_back(background);
}
background->addRect(glyphBounds);
}
}
curX += advance;
}
if (lineIndex == ellipsisLine)
{
goto skipLines;
}
lineIndexInParagraph++;
lineIndex++;
}
if (!paragraphLines.empty())
{
curY += paragraphLines.back().bottom;
}
curY += fitParagraphSpacing();
}
skipLines:
// Step 7: consider fit mode, and update local transform
auto scale = 1.0f;
auto xOffset = -m_bounds.width() * originX();
auto yOffset = -m_bounds.height() * originY();
if (overflow() == TextOverflow::fit)
{
auto xScale = ((effectiveSizing() != TextSizing::autoWidth ||
overflowAsFixed()) &&
maxWidth > m_bounds.width())
? m_bounds.width() / maxWidth
: 1;
auto baseline = fitFromBaseline() ? m_lines[0][0].baseline : 0;
auto yScale = (overflowAsFixed() && totalHeight > m_bounds.height())
? (m_bounds.height() - baseline) /
(totalHeight - baseline)
: 1;
if (xScale != 1 || yScale != 1)
{
scale = std::max(0.0f, xScale > yScale ? yScale : xScale);
yOffset += baseline * (1 - scale);
switch (align())
{
case TextAlign::center:
xOffset += (m_bounds.width() - maxWidth * scale) / 2 - minX * scale;
break;
case TextAlign::right:
xOffset += m_bounds.width() - maxWidth * scale - minX * scale;
break;
default:
break;
}
}
}
if (verticalAlign() != VerticalTextAlign::top)
{
if (overflowAsFixed())
{
yOffset = -m_bounds.height() * originY();
if (verticalAlign() == VerticalTextAlign::middle)
{
yOffset += (m_bounds.height() - totalHeight * scale) / 2;
}
else if (verticalAlign() == VerticalTextAlign::bottom)
{
yOffset += m_bounds.height() - totalHeight * scale;
}
}
}
m_transform = Mat2D::fromScaleAndTranslation(scale, scale, xOffset, yOffset);
#ifdef WITH_RIVE_LAYOUT
// The layout sizes this text by measuring it, and a change to anything it
// measures marks the layout dirty first (markShapeDirty), so the solve
// that just ran already saw this shape. Re-marking it unconditionally ran
// a second full solve after every text change, and one after every paint
// change too, since a recolor rebuilds the render styles. It is still owed
// when the shape and the measurement disagree: a modifier reshaped the
// text without telling the layout, or the solve handed us scale types or
// a direction that change what measure() reports. A solve that sizes us
// from the style alone (a fixed or filled slot) never calls measure(), so
// the key can go stale with nothing owed: ask once, and a solve that
// measures us records a fresh key, while one that doesn't leaves 0, which
// stops us asking again until the next measure.
if (m_measuredKey != 0 && m_shapedKey != m_measuredKey)
{
m_measuredKey = 0;
markLayoutNodeDirty();
}
#endif
// Step 8: cleanup
for (TextValueRun* textValueRun : m_allRuns)
{
if (textValueRun->isHitTarget())
{
textValueRun->computeHitContours();
}
}
for (TextStylePaint* style : m_textStylePaints)
{
TextStyleBackground* background = style->background();
if (background != nullptr)
{
background->updatePath();
background->propagateOpacity(renderOpacity());
}
}
for (TextStyleBackground* background : m_globalBackgrounds)
{
background->updatePath();
background->propagateOpacity(renderOpacity());
}
// Drop the geometry of shared paints this build no longer drew.
m_sharedGeometry.erase(
std::remove_if(
m_sharedGeometry.begin(),
m_sharedGeometry.end(),
[](const std::pair<
TextStylePaint*,
std::unique_ptr<TextStyleGeometry>>& entry) {
return !entry.second->hasContents();
}),
m_sharedGeometry.end());
}
TextStyleGeometry& Text::sharedGeometry(TextStylePaint* paint)
{
for (auto& entry : m_sharedGeometry)
{
if (entry.first == paint)
{
return *entry.second;
}
}
m_sharedGeometry.emplace_back(paint, std::make_unique<TextStyleGeometry>());
return *m_sharedGeometry.back().second;
}
const TextStylePaint* Text::styleFromShaperId(uint16_t id) const
{
// Shaper ids index every run, list-driven ones included.
assert(id < m_allRuns.size());
return m_allRuns[id]->style();
}
void Text::draw(Renderer* renderer)
{
if (needsSaveOperation())
{
renderer->save();
}
// For now we need to check both empty() and hasRenderPath() in
// ShapePaintPath because the raw path gets cleared when the render path
// is created.
if (overflow() == TextOverflow::clipped &&
(!m_clipPath.empty() || m_clipPath.hasRenderPath()))
{
renderer->clipPath(m_clipPath.renderPath(this));
}
auto worldTransform = shapeWorldTransform();
// Backgrounds first so they sit behind every glyph, in style child order.
for (TextStylePaint* style : m_textStylePaints)
{
TextStyleBackground* background = style->background();
if (background != nullptr)
{
background->draw(renderer, worldTransform);
}
}
for (TextStyleBackground* background : m_globalBackgrounds)
{
background->draw(renderer, worldTransform);
}
if (m_selectionController != nullptr)
{
m_selectionController->draw(this, renderer);
}
for (auto& cmd : m_drawCommands)
{
if (cmd.type == TextDrawCommand::kStylePath)
{
if (cmd.geometry != nullptr)
{
cmd.style->drawGeometry(
renderer,
worldTransform,
*cmd.geometry,
renderOpacity());
}
else
{
cmd.style->draw(renderer, worldTransform);
}
}
else
{
drawColorGlyph(renderer, cmd.colorGlyph, worldTransform);
}
}
if (needsSaveOperation())
{
renderer->restore();
}
}
void rive::drawColorGlyphLayer(
Renderer* renderer,
Factory* factory,
Font::ColorGlyphLayer& layer,
float opacity)
{
using PaintType = Font::ColorGlyphPaintType;
auto paint = factory->makeRenderPaint();
paint->style(RenderPaintStyle::fill);
Mat2D toUnitCircle;
bool isLinear = layer.paintType == PaintType::linearGradient;
bool isRadial = layer.paintType == PaintType::radialGradient &&
layer.radialTransform.invert(&toUnitCircle);
if (layer.stops.size() < 2 || (!isLinear && !isRadial))
{
ColorInt color = layer.paintType == PaintType::solid || layer.stops.empty()
? layer.color
: layer.stops.front().color;
paint->color(colorModulateOpacity(color, opacity));
renderer->drawPath(
factory->makeRenderPath(layer.path, FillRule::nonZero).get(),
paint.get());
return;
}
std::vector<ColorInt> colors;
std::vector<float> stops;
colors.reserve(layer.stops.size());
stops.reserve(layer.stops.size());
for (const auto& stop : layer.stops)
{
colors.push_back(colorModulateOpacity(stop.color, opacity));
stops.push_back(stop.offset);
}
if (isLinear)
{
paint->shader(factory->makeLinearGradient(
layer.x0,
layer.y0,
layer.x1,
layer.y1,
colors.data(),
stops.data(),
colors.size()));
renderer->drawPath(
factory->makeRenderPath(layer.path, FillRule::nonZero).get(),
paint.get());
return;
}
// Radial shaders are circles, so draw in the gradient's own space.
paint->shader(factory->makeRadialGradient(
0,
0,
1,
colors.data(),
stops.data(),
colors.size()));
layer.path.transformInPlace(toUnitCircle);
renderer->save();
renderer->transform(layer.radialTransform);
renderer->drawPath(
factory->makeRenderPath(layer.path, FillRule::nonZero).get(),
paint.get());
renderer->restore();
}
void Text::drawColorGlyph(
Renderer* renderer,
const TextDrawCommand::ColorGlyphInfo& info,
const Mat2D& worldTransform)
{
std::vector<Font::ColorGlyphLayer> layers;
size_t count = info.font->getColorLayers(
info.glyphId,
layers,
info.foregroundColor);
if (count == 0)
{
return;
}
Factory* factory = artboard()->factory();
renderer->save();
renderer->transform(worldTransform * info.transform);
for (auto& layer : layers)
{
if (layer.paintType == Font::ColorGlyphPaintType::image)
{
// Decode and draw bitmap emoji (SBIX/CBDT).
ColorGlyphCacheKey cacheKey{info.font.get(), info.glyphId};
auto it = m_emojiImageCache.find(cacheKey);
if (it == m_emojiImageCache.end())
{
auto image = factory->decodeImage(
{layer.imageBytes.data(), layer.imageBytes.size()});
it = m_emojiImageCache.emplace(cacheKey, std::move(image)).first;
}
if (it->second != nullptr)
{
renderer->save();
// Transform from glyph space: position at bearing and
// scale from image pixels to glyph extent units.
float scaleX = layer.imageExtentX / (float)layer.imageWidth;
float scaleY = layer.imageExtentY / (float)layer.imageHeight;
renderer->transform(Mat2D(
scaleX,
0,
0,
scaleY,
layer.imageBearingX,
layer.imageBearingY));
renderer->drawImage(
it->second.get(),
ImageSampler::LinearClamp(),
BlendMode::srcOver,
info.opacity);
renderer->restore();
}
}
else
{
drawColorGlyphLayer(renderer, factory, layer, info.opacity);
}
}
renderer->restore();
}
void Text::addRun(TextValueRun* run)
{
m_runs.push_back(run);
m_allRuns.push_back(run);
}
#ifdef WITH_RIVE_EDITOR
void Text::sortRunsForEditor(
const std::function<bool(TextValueRun*, TextValueRun*)>& cmp)
{
if (std::is_sorted(m_runs.begin(), m_runs.end(), cmp) &&
std::is_sorted(m_allRuns.begin(), m_allRuns.end(), cmp))
{
return;
}
std::sort(m_runs.begin(), m_runs.end(), cmp);
std::sort(m_allRuns.begin(), m_allRuns.end(), cmp);
markShapeDirty();
}
#endif
void Text::addModifierGroup(TextModifierGroup* group)
{
m_modifierGroups.push_back(group);
}
void Text::markShapeDirty() { markShapeDirty(true); }
void Text::markShapeDirty(bool sendToLayout)
{
m_listenedRunsDirty = true;
m_listItemsDirty = true;
addDirt(ComponentDirt::Path);
for (TextModifierGroup* group : m_modifierGroups)
{
group->clearRangeMaps();
}
markWorldTransformDirty();
#ifdef WITH_RIVE_LAYOUT
if (sendToLayout)
{
markLayoutNodeDirty();
}
#endif
}
void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); }
void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); }
void Text::styleReferenceWillChange()
{
// The render list and draw commands hold raw pointers into the outgoing
// style instance's paint; drop them before it is destroyed.
clearRenderStyles();
m_sharedGeometry.clear();
markShapeDirty();
}
void Text::markListenedRuns()
{
if (!m_listenedRunsDirty)
{
return;
}
m_listenedRunsDirty = false;
Artboard* host = artboard();
// Only instances run state machines; the file's own artboards don't.
if (host == nullptr || !host->isInstance())
{
return;
}
for (auto run : m_allRuns)
{
auto style = run->styleReference();
if (style != nullptr && style->listensToRuns())
{
run->isHitTarget(true);
}
}
}
bool Text::mayHaveListeningRuns()
{
for (auto child : children())
{
if (child->is<TextStyleReference>() &&
child->as<TextStyleReference>()->listensToRuns())
{
return true;
}
}
// List runs (and bound handles) may pick any of the file's styles.
return m_file != nullptr && m_file->textStyleShares().anyListensToRuns();
}
void Text::syncStyleListItems()
{
if (!m_listItemsDirty)
{
return;
}
m_listItemsDirty = false;
Artboard* host = artboard();
// Only instances bind their styles; the file's own artboards don't.
if (host == nullptr || !host->isInstance())
{
return;
}
// The list item of the one run drawn with [style], if it's a list run.
auto itemFor = [this](const TextStyleReference* style) {
TextValueRun* only = nullptr;
for (auto run : m_allRuns)
{
if (run->styleReference() != style)
{
continue;
}
if (only != nullptr)
{
return rcp<ViewModelInstance>(nullptr);
}
only = run;
}
for (auto listener : m_valueRunListeners)
{
if (only != nullptr && listener->textValueRun() == only)
{
return listener->viewModelInstance();
}
}
return rcp<ViewModelInstance>(nullptr);
};
for (auto child : children())
{
if (child->is<TextStyleReference>())
{
auto style = child->as<TextStyleReference>();
style->listItem(itemFor(style));
}
}
for (auto& style : m_onDemandStyles)
{
style->listItem(itemFor(style.get()));
}
}
void Text::runStateMachines(std::vector<StateMachineInstance*>& out)
{
for (auto child : children())
{
if (child->is<TextStyleReference>())
{
if (auto stateMachine = child->as<TextStyleReference>()
->runStateMachine())
{
out.push_back(stateMachine);
}
}
}
for (auto& style : m_onDemandStyles)
{
if (auto stateMachine = style->runStateMachine())
{
out.push_back(stateMachine);
}
}
}
void Text::styleReferenceDidChange()
{
// A handle now shows a different style: list runs that matched it by
// style, and runs bound to one, may need another handle or an on-demand
// style instead.
resolveListRunStyles();
for (auto run : m_runs)
{
if (run->isStyleBound())
{
run->resolveStyle();
}
}
pruneOnDemandStyles();
markShapeDirty();
}
TextStyleReference* Text::boundRunStyle(
ViewModelInstanceArtboard* value,
const TextValueRun* run)
{
return globalStyleReference(value, nullptr, run);
}
void Text::runStyleChanged()
{
pruneOnDemandStyles();
markShapeDirty();
}
TextStyleReference* Text::resolveListRunStyle(ViewModelInstance* instance)
{
if (instance == nullptr)
{
return nullptr;
}
auto globalValue = instance->propertyValue(SymbolType::globalTextStyle);
auto nameValue = instance->propertyValue(SymbolType::textStyle);
if (globalValue != nullptr && globalValue->is<ViewModelInstanceArtboard>())
{
auto style = globalStyleReference(
globalValue->as<ViewModelInstanceArtboard>(),
instance);
if (style != nullptr)
{
return style;
}
}
// An item with no style symbol keeps no style, as before.
if (globalValue == nullptr && nameValue == nullptr)
{
return nullptr;
}
if (nameValue != nullptr && nameValue->is<ViewModelInstanceString>())
{
auto style = styleReferenceNamed(
nameValue->as<ViewModelInstanceString>()->propertyValue());
if (style != nullptr)
{
return style;
}
}
return firstStyleReference();
}
TextStyleReference* Text::globalStyleReference(
ViewModelInstanceArtboard* value,
ViewModelInstance* item,
const TextValueRun* owner)
{
// The style artboard the value names: a runtime-set BindableArtboard
// (normalized to its file-owned source), else a file artboard index.
const Artboard* source = nullptr;
rcp<const File> crossFile;
if (value->asset() != nullptr)
{
auto artboard = value->asset()->artboard();
if (artboard != nullptr)
{
source = artboard->artboardSource();
auto assetFile = artboard->file();
if (assetFile != nullptr && assetFile.get() != m_file)
{
crossFile = assetFile;
}
}
}
else if (m_file != nullptr && value->propertyValue() != (uint32_t)-1)
{
source = m_file->artboard(value->propertyValue());
}
// Not a text style: let the caller fall back.
if (source == nullptr ||
TextStyleGlobal::firstTextStylePaint(source) == nullptr)
{
return nullptr;
}
// A style with a state machine keeps state of its own: each list item
// (and each run bound to it) gets an instance of its own, which reads that
// item (see syncStyleListItems). Other styles are shared by the runs
// naming them.
TextStyleShareInfo info = crossFile == nullptr && m_file != nullptr
? m_file->textStyleShares().info(source)
: TextStyleShareCache::analyze(
source,
crossFile.get());
rcp<ViewModelInstance> perItem = info.hasStateMachine ? ref_rcp(item)
: nullptr;
const TextValueRun* perRun = info.hasStateMachine ? owner : nullptr;
auto bound = value->boundViewModelInstance();
// The values an item's own instance copies.
rcp<ViewModelInstance> handleValues;
if (bound == nullptr)
{
// A handle on this Text showing that style keeps its per-text
// exposed values: the runs share it, or each item's own instance
// copies them (what its state machine writes stays its own).
for (auto child : children())
{
if (child->is<TextStyleGlobal>() &&
child->as<TextStyleGlobal>()->instanceSource() == source)
{
auto handle = child->as<TextStyleGlobal>();
if (perItem == nullptr && perRun == nullptr)
{
return handle;
}
handleValues = handle->styleViewModelInstance();
break;
}
}
}
for (auto& style : m_onDemandStyles)
{
if (style->source() == source &&
style->boundViewModelInstance() == bound.get() &&
style->item() == perItem.get() && style->owner() == perRun)
{
return style.get();
}
}
auto host = artboard();
m_onDemandStyles.push_back(std::make_unique<TextStyleOnDemand>(
source,
bound,
m_file,
crossFile,
host != nullptr ? host->factory() : nullptr,
host != nullptr ? host->dataContext() : nullptr,
this,
perItem,
handleValues,
perRun));
return m_onDemandStyles.back().get();
}
void Text::hostDataContext(rcp<DataContext> value)
{
for (auto& style : m_onDemandStyles)
{
style->hostDataContext(value);
}
}
void Text::relinkHostDataContext()
{
for (auto& style : m_onDemandStyles)
{
style->relinkHostDataContext();
}
}
void Text::updateHostedDataBinds()
{
for (auto& style : m_onDemandStyles)
{
style->updateHostedDataBinds();
}
}
TextStyleReference* Text::styleReferenceNamed(const std::string& name)
{
for (auto child : children())
{
if (child->is<TextStyleReference>() && child->name() == name)
{
return child->as<TextStyleReference>();
}
}
return nullptr;
}
TextStyleReference* Text::firstStyleReference()
{
buildTextStylePaints();
if (!m_textStylePaints.empty())
{
return m_textStylePaints.front();
}
for (auto child : children())
{
if (child->is<TextStyleReference>())
{
return child->as<TextStyleReference>();
}
}
return nullptr;
}
void Text::resolveListRunStyles()
{
for (auto listener : m_valueRunListeners)
{
listener->resolveStyle();
}
}
void Text::pruneOnDemandStyles()
{
if (m_onDemandStyles.empty())
{
return;
}
std::unordered_set<const TextStyleReference*> used;
// Runs bound to a style draw one too.
for (auto run : m_runs)
{
used.insert(run->styleReference());
}
for (auto listener : m_valueRunListeners)
{
auto run = listener->textValueRun();
if (run != nullptr)
{
used.insert(run->styleReference());
}
}
auto unused = [&](const std::unique_ptr<TextStyleOnDemand>& style) {
return used.count(style.get()) == 0;
};
if (std::none_of(m_onDemandStyles.begin(), m_onDemandStyles.end(), unused))
{
return;
}
// The render caches hold raw pointers into the paints about to go.
clearRenderStyles();
m_sharedGeometry.clear();
m_onDemandStyles.erase(
std::remove_if(m_onDemandStyles.begin(), m_onDemandStyles.end(), unused),
m_onDemandStyles.end());
markShapeDirty();
}
bool Text::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
syncStyleListItems();
bool keepGoing = false;
bool changed = false;
for (auto& style : m_onDemandStyles)
{
if (style->advance(elapsedSeconds, flags, changed))
{
keepGoing = true;
}
}
if (changed)
{
// Shape-affecting style properties (font, size, spacing) may have
// changed.
markShapeDirty();
}
return keepGoing;
}
AdvancingComponent::QuietState Text::quietState()
{
auto state = QuietState::quiet;
for (auto& style : m_onDemandStyles)
{
switch (style->quietState())
{
case QuietState::never:
return QuietState::never;
case QuietState::busy:
state = QuietState::busy;
break;
case QuietState::quiet:
break;
}
}
return state;
}
void Text::alignValueChanged() { markShapeDirty(); }
void Text::sizingValueChanged() { markShapeDirty(); }
void Text::wordBreakValueChanged() { markShapeDirty(); }
void Text::fitFontSizeResizesBoxChanged()
{
// Only fitFontSize consults the flag, and only to decide what measure()
// reports, so nothing else needs re-shaping.
if (overflow() == TextOverflow::fitFontSize)
{
markShapeDirty();
}
}
void Text::overflowValueChanged()
{
if (effectiveSizing() != TextSizing::autoWidth || overflowAsFixed())
{
markShapeDirty();
}
}
void Text::widthChanged()
{
if (effectiveSizing() != TextSizing::autoWidth)
{
markShapeDirty();
}
}
void Text::paragraphSpacingChanged()
{
if (overflow() == TextOverflow::fitFontSize)
{
markShapeDirty();
}
else
{
markPaintDirty();
// The spacing needs no reshape, but measure() adds it between
// paragraphs, so a layout sizing this text has to measure it again.
// buildRenderStyles no longer re-marks the layout on every rebuild.
#ifdef WITH_RIVE_LAYOUT
markLayoutNodeDirty();
#endif
}
}
void Text::heightChanged()
{
if (effectiveSizing() == 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.data();
const uint8_t* end = ptr + text.size();
uint32_t n = 0;
while (ptr < end && *ptr)
{
m_value.push_back(UTF::NextUTF8(&ptr, end));
n += 1;
}
// Shaping takes no empty runs.
if (n == 0)
{
return;
}
m_runs.push_back(
{std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
}
bool Text::makeStyled(
StyledText& styledText,
bool withModifiers,
float fontScale) const
{
styledText.clear();
uint16_t runIndex = 0;
for (auto valueRun : m_allRuns)
{
auto style = valueRun->style();
const std::string& text = valueRun->text();
if (style == nullptr || style->font() == nullptr || text.empty())
{
runIndex++;
continue;
}
// A custom line height (>= 0) is an absolute value that overrides the
// font-metric spacing, so it must scale with the font to keep
// fitFontScale a true uniform scale of the authored layout. The -1
// sentinel ("derive from font metrics") is left untouched. Letter
// spacing is likewise an absolute advance added after glyph shaping, so
// it scales for the same reason.
float lineHeight = style->lineHeight();
if (lineHeight >= 0.0f)
{
lineHeight *= fontScale;
}
styledText.append(
style->font(),
style->fontSize() * fontScale,
lineHeight,
style->letterSpacing() * fontScale,
text,
runIndex++);
}
if (withModifiers)
{
for (TextModifierGroup* group : m_modifierGroups)
{
group->applyShapeModifiers(*this, styledText);
}
}
return !styledText.empty();
}
SimpleArray<SimpleArray<GlyphLine>> Text::BreakLines(
const SimpleArray<Paragraph>& paragraphs,
float width,
TextAlign align,
TextWrap wrap,
TextWordBreak wordBreak,
float minAlignWidth)
{
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 || wrap == TextWrap::noWrap) ? -1.0f : width,
wordBreak);
if (autoWidth)
{
paragraphWidth = std::max(
paragraphWidth,
GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs));
}
paragraphIndex++;
}
// Align within the field when it's wider than the text; text that
// overflows the field aligns within itself (effectively left aligned).
paragraphWidth = std::max(paragraphWidth, minAlignWidth);
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::onDirty(ComponentDirt value)
{
// Sometimes a WorldTransform dirt may also affect Path
if (hasDirt(value, ComponentDirt::WorldTransform))
{
for (TextModifierGroup* modifierGroup : m_modifierGroups)
{
modifierGroup->onTextWorldTransformDirty();
}
}
if (hasDirt(value, ComponentDirt::Path | ComponentDirt::Paint))
{
for (TextStylePaint* style : m_renderStyles)
{
style->invalidateStrokeEffects();
}
}
}
void Text::update(ComponentDirt value)
{
Super::update(value);
markListenedRuns();
syncStyleListItems();
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();
bool parentIsLayoutNotArtboard = parent()->is<LayoutComponent>() &&
!parent()->is<Artboard>();
// For fitFontSize, find the largest integer font size that fits the
// bounds, then shape/lay out the text at that size below.
float fontScale = overflow() == TextOverflow::fitFontSize ? fitFontScale()
: 1.0f;
m_fitFontScale = fontScale;
if (precomputeModifierCoverage &&
makeStyled(m_modifierStyledText, false, fontScale))
{
const auto& runs = m_modifierStyledText.runs();
m_modifierShape = runs[0].font->shapeText(
m_modifierStyledText.unichars(),
runs);
m_modifierLines = BreakLines(
m_modifierShape,
(effectiveSizing() == TextSizing::autoWidth &&
!parentIsLayoutNotArtboard)
? -1.0f
: effectiveWidth(),
align(),
wrap(),
wordBreak());
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);
}
}
const bool styled = makeStyled(m_styledText, true, fontScale);
m_shapedKey = measureKey(m_styledText, fontScale);
if (styled)
{
const auto& runs = m_styledText.runs();
m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
m_lines = BreakLines(
m_shape,
(effectiveSizing() == TextSizing::autoWidth &&
!parentIsLayoutNotArtboard)
? -1.0f
: effectiveWidth(),
align(),
wrap(),
wordBreak());
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_ellipsisRun = {};
m_emojiImageCache.clear();
// 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 (TextStylePaint* style : m_renderStyles)
{
style->propagateOpacity(renderOpacity());
}
for (TextStylePaint* style : m_textStylePaints)
{
if (style->background() != nullptr)
{
style->background()->propagateOpacity(renderOpacity());
}
}
}
if (hasDirt(
value,
ComponentDirt::WorldTransform | ComponentDirt::Path |
ComponentDirt::Paint))
{
m_clipPath.rewind();
m_shapeWorldTransform = m_WorldTransform * m_transform;
m_clipPath.addPath(m_clipRect, &m_shapeWorldTransform);
}
if (m_selectionController != nullptr &&
hasDirt(value, ComponentDirt::Path | ComponentDirt::Paint))
{
m_selectionController->textUpdated(
this,
hasDirt(value, ComponentDirt::Path));
}
}
// What measure() reports depends on, as a hash: the styled text it shapes,
// the scale it shapes at, and the two inputs it reads that a layout hands us
// after its solve (controlSize) -- the effective sizing, from the slot's scale
// types, and the alignment, from its direction. Everything else it reads is
// authored, and changing that marks the layout dirty on its own.
//
// Mixed eight bytes at a time. Every step is a bijection of the running hash,
// so two keys that differ in a single word always differ; hashing byte by
// byte cost about a nanosecond a byte, next to the shaping it sits beside.
uint64_t Text::measureKey(const StyledText& styled, float fontScale) const
{
uint64_t hash = 0x9e3779b97f4a7c15ull;
auto mix = [&hash](uint64_t word) {
hash ^= word;
hash *= 0xff51afd7ed558ccdull;
hash ^= hash >> 32;
};
auto bits = [](float value) {
uint32_t out;
memcpy(&out, &value, sizeof(out));
return (uint64_t)out;
};
mix(bits(fontScale) | (uint64_t)(uint8_t)effectiveSizing() << 32 |
(uint64_t)(uint8_t)align() << 40);
for (const TextRun& run : styled.runs())
{
mix((uint64_t)(uintptr_t)run.font.get());
mix(bits(run.size) | bits(run.lineHeight) << 32);
mix(bits(run.letterSpacing) | (uint64_t)run.unicharCount << 32);
mix((uint64_t)run.script | (uint64_t)run.styleId << 32 |
(uint64_t)run.level << 48);
}
const auto& unichars = styled.unichars();
mix(unichars.size());
size_t i = 0;
for (; i + 1 < unichars.size(); i += 2)
{
mix((uint64_t)unichars[i] | (uint64_t)unichars[i + 1] << 32);
}
if (i < unichars.size())
{
mix(unichars[i]);
}
return hash;
}
Vec2D Text::measure(Vec2D maxSize)
{
auto measuringWidth = 0.0f;
switch (effectiveSizing())
{
case TextSizing::autoHeight:
case TextSizing::fixed:
// The authored width only governs while the text owns its width
// axis. Under a layout that boxes it -- fill, or a fixed slot the
// layout resolves -- the authored value is stale and the real
// width is the one being offered in maxSize, so leave it
// unbounded and let the min() below take the offer. Reading
// m_layoutWidth instead would not work: controlSize writes it
// after the solve, so it is still NAN here.
measuringWidth = layoutOwnsWidth() ? std::numeric_limits<float>::max()
: width();
break;
default:
measuringWidth = std::numeric_limits<float>::max();
break;
}
// fitFontSize shrinks the font until the text fits its box, so the box we
// report back to the layout engine has to be measured at the *fitted*
// size. Measuring at the authored size hands the layout a slot taller
// than the text that will actually be drawn into it, which shows up as
// dead space under a hug-height text. The fit runs against the space the
// layout is offering here (maxSize/measuringWidth), not against
// m_layoutWidth/Height -- those still hold the previous pass's slot.
const float fitWidth = std::min(maxSize.x, measuringWidth);
float fontScale = 1.0f;
// Gate this the way fitFontScale() gates the update side -- overflow is
// inert below 7.3 (FileFeatures::layoutSizesBox) -- minus one clause.
if (overflow() == TextOverflow::fitFontSize &&
fitFontSizeResizesBoxActive() &&
hasFileFeature(FileFeatures::layoutSizesBox) &&
fitWidth != std::numeric_limits<float>::max())
{
fontScale = fitFontScale(fitWidth, maxSize.y);
}
const bool styled = makeStyled(m_styledText, true, fontScale);
m_measuredKey = measureKey(m_styledText, fontScale);
if (styled)
{
const float paragraphSpace = paragraphSpacing() * fontScale;
const auto& runs = m_styledText.runs();
auto shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
auto measuringWrap = maxSize.x == std::numeric_limits<float>::max() &&
effectiveSizing() != TextSizing::autoHeight
? TextWrap::noWrap
: wrap();
auto lines = BreakLines(
shape,
fitWidth,
align(),
measuringWrap,
wordBreak());
float y = 0;
float computedHeight = 0.0f;
float minY = 0;
int paragraphIndex = 0;
float maxWidth = 0;
if (textOrigin() == TextOrigin::baseline && !lines.empty() &&
!lines[0].empty())
{
y -= lines[0][0].baseline;
minY = y;
}
int ellipsisLine = -1;
bool wantEllipsis = overflow() == TextOverflow::ellipsis &&
sizing() == TextSizing::fixed;
for (const SimpleArray<GlyphLine>& paragraphLines : lines)
{
const Paragraph& paragraph = 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];
if (width > maxWidth)
{
maxWidth = width;
}
if (wantEllipsis && y + line.bottom > maxSize.y)
{
if (ellipsisLine == -1)
{
// Nothing fits, just show the first line and ellipse
// it.
computedHeight = y + line.bottom;
}
goto doneMeasuring;
}
ellipsisLine++;
computedHeight = y + line.bottom;
}
if (!paragraphLines.empty())
{
y += paragraphLines.back().bottom;
}
y += paragraphSpace;
}
doneMeasuring:
// Match the rendered box: trim the ascent/descent band for auto sizing.
float topTrim = 0.0f;
float bottomTrim = 0.0f;
computeVerticalTrim(
lines,
shape,
verticalTrimTop(),
verticalTrimBottom(),
topTrim,
bottomTrim);
Vec2D bounds;
switch (sizing())
{
case TextSizing::autoWidth:
bounds = Vec2D(
maxWidth,
std::max(minY, computedHeight - topTrim - bottomTrim));
break;
case TextSizing::autoHeight:
bounds = Vec2D(
width(),
std::max(minY, computedHeight - topTrim - bottomTrim));
break;
case TextSizing::fixed:
bounds = Vec2D(width(), minY + height());
break;
}
return Vec2D(std::min(maxSize.x, bounds.x), std::min(maxSize.y, bounds.y));
}
return Vec2D();
}
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();
// A baseline origin moves the first line up, which measure() reports as a
// different height, so a layout sizing this text has to measure it again.
#ifdef WITH_RIVE_LAYOUT
markLayoutNodeDirty();
#endif
}
void Text::originXChanged()
{
markPaintDirty();
markWorldTransformDirty();
}
void Text::originYChanged()
{
markPaintDirty();
markWorldTransformDirty();
}
void Text::verticalTrimValueChanged()
{
// Trim affects both the rendered box and the layout-measured size, so go
// through the shape/layout path like sizing does.
markShapeDirty();
}
#else
// Text disabled.
Text::~Text() {}
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(bool sendToLayout) {}
void Text::markShapeDirty() {}
void Text::styleReferenceWillChange() {}
void Text::styleReferenceDidChange() {}
void Text::hostDataContext(rcp<DataContext> value) {}
void Text::relinkHostDataContext() {}
void Text::updateHostedDataBinds() {}
bool Text::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
return false;
}
AdvancingComponent::QuietState Text::quietState() { return QuietState::quiet; }
void Text::update(ComponentDirt value) {}
void Text::onDirty(ComponentDirt value) {}
void Text::alignValueChanged() {}
void Text::sizingValueChanged() {}
void Text::wordBreakValueChanged() {}
void Text::overflowValueChanged() {}
void Text::fitFontSizeResizesBoxChanged() {}
void Text::widthChanged() {}
void Text::heightChanged() {}
void Text::markPaintDirty() {}
void Text::modifierShapeDirty() {}
bool Text::modifierRangesNeedShape() const { return false; }
const TextStylePaint* 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() {}
void Text::verticalTrimValueChanged() {}
Vec2D Text::measureLayout(
float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
return Vec2D();
}
void Text::controlSize(
Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction)
{}
#endif
StatusCode Text::import(ImportStack& importStack)
{
// A layout has always sized the text itself (effectiveWidth/Height), but
// before 7.3 an auto-sized text still took its *box* from the content, so
// the box could disagree with the slot and every overflow mode stayed
// inert. Keep that for those files; newer ones box to the slot. See
// File::minorVersion.
int major = importStack.majorVersion();
int minor = importStack.minorVersion();
auto atLeast = [major, minor](int wantMinor) {
return major > 7 || (major == 7 && minor >= wantMinor);
};
m_fileFeatures = FileFeatures::none;
if (atLeast(3))
{
m_fileFeatures |= FileFeatures::layoutSizesBox;
}
// 7.4 measures a fitFontSize text at its fitted size. Older files laid
// their layouts out against the unshrunk box, so honoring the flag there
// would reflow them; fitFontSizeResizesBox defaults to true and those
// files never wrote it.
if (atLeast(4))
{
m_fileFeatures |= FileFeatures::fitFontSizeResizesBox;
}
return Super::import(importStack);
}
Core* Text::clone() const
{
Text* twin = TextBase::clone()->as<Text>();
twin->m_fileFeatures = m_fileFeatures;
twin->m_file = m_file;
return twin;
}
Vec2D Text::layoutBaseTranslation(LayoutParticipant* participant) const
{
assert(participant != nullptr);
return Vec2D(
participant->resolvedLeft() + originX() * participant->resolvedWidth(),
participant->resolvedTop() + originY() * participant->resolvedHeight());
}
void Text::composeWorldTransform()
{
// The base composes m_WorldTransform = parentWorld * m_Transform; insert
// the origin-based slot base instead when participating (text sizes via its
// own layout, not a geometric scale).
#ifdef WITH_RIVE_LAYOUT
auto* participant = layoutParticipant();
if (participant != nullptr && m_ParentTransformComponent != nullptr)
{
Mat2D base = Mat2D::fromTranslation(layoutBaseTranslation(participant));
m_WorldTransform = m_ParentTransformComponent->worldTransform() * base *
m_Transform;
return;
}
#endif
Super::composeWorldTransform();
}
void Text::updateConstraints()
{
#ifdef WITH_RIVE_LAYOUT
auto* participant = layoutParticipant();
if (participant != nullptr)
{
participant->applyLayoutConstraints();
}
#endif
Super::updateConstraints();
}
LayoutParticipant* Text::layoutParticipant() const
{
for (auto* child : children())
{
if (child->is<LayoutParticipant>())
{
return child->as<LayoutParticipant>();
}
}
return nullptr;
}
bool Text::isParticipatingInLayout() const
{
return layoutParticipant() != nullptr;
}
bool Text::layoutOwnsWidth() const
{
#ifdef WITH_RIVE_LAYOUT
if (auto* participant = layoutParticipant())
{
const auto widthScaleType = (LayoutScaleType)
participant->layoutWidthScaleType();
return widthScaleType == LayoutScaleType::fixed ||
widthScaleType == LayoutScaleType::fill;
}
#endif
return false;
}
TextAlign Text::align() const
{
auto val = (TextAlign)alignValue();
if (m_layoutDirection == LayoutDirection::inherit || val == TextAlign::center)
{
return val;
}
return m_layoutDirection == LayoutDirection::ltr ? TextAlign::left
: TextAlign::right;
}
void Text::updateList(std::vector<rcp<ViewModelInstanceListItem>>* list)
{
#ifdef WITH_RIVE_TEXT
buildTextStylePaints();
m_allRuns.clear();
m_allRuns.assign(m_runs.begin(), m_runs.end());
auto currentSize = m_valueRunListeners.size();
size_t index = 0;
for (auto& listItem : *list)
{
auto instance = listItem->viewModelInstance();
if (instance)
{
TextValueRun* textRun;
if (index < currentSize)
{
auto textRunListener = m_valueRunListeners[index];
textRunListener->remap(instance);
textRun = textRunListener->textValueRun();
}
else
{
textRun = new TextValueRun();
textRun->textComponent(this);
auto textRunListener = new TextValueRunListener(
textRun,
instance,
this);
m_valueRunListeners.push_back(textRunListener);
}
if (textRun)
{
m_allRuns.push_back(textRun);
}
index++;
}
}
while (m_valueRunListeners.size() > index)
{
auto valueRun = m_valueRunListeners.back();
m_valueRunListeners.pop_back();
delete valueRun;
}
pruneOnDemandStyles();
markShapeDirty();
#endif
}
#ifdef WITH_RIVE_TEXT
void Text::buildTextStylePaints()
{
if (m_textStylePaints.size() == 0)
{
for (auto& child : children())
{
if (child->coreType() == TextStylePaint::typeKey)
{
m_textStylePaints.push_back(child->as<TextStylePaint>());
}
}
}
}
#endif
BoundsFidelity Text::paintedWorldBounds(AABB* out)
{
const BoundsFidelity fidelity = paintedBoundsFromLocal(
localBounds(),
worldTransform(),
nullptr,
out);
if (fidelity == BoundsFidelity::none)
{
return fidelity;
}
// Never better than approximate: see the note on the declaration. The ink
// can reach past the layout box, so an exact claim here would crop glyphs.
return BoundsFidelity::approximate;
}
#ifdef WITH_RIVE_EDITOR
void Text::editorPrepareForDestruction()
{
#ifdef WITH_RIVE_TEXT
if (m_selectionController != nullptr)
m_selectionController->remove(this);
for (auto* listener : m_valueRunListeners)
delete listener;
m_valueRunListeners.clear();
#endif
Component::editorPrepareForDestruction();
}
#endif