| #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 |