blob: 46e84603434306143af509fc183ec51ea8138112 [file]
#ifndef _RIVE_TEXT_CORE_HPP_
#define _RIVE_TEXT_CORE_HPP_
#include "rive/generated/text/text_base.hpp"
#include "rive/math/aabb.hpp"
#include "rive/text/text_value_run.hpp"
#include "rive/text_engine.hpp"
#include "rive/shapes/shape_paint_path.hpp"
#include "rive/simple_array.hpp"
#include "rive/text/glyph_lookup.hpp"
#include "rive/text/text_interface.hpp"
#include "rive/text/text_style_geometry.hpp"
#include "rive/text/text_style_on_demand.hpp"
#include "rive/data_bind/host_data_context_consumer.hpp"
#include "rive/advancing_component.hpp"
#include "rive/data_bind/data_bind_list_item_consumer.hpp"
#include "rive/viewmodel/symbol_type.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include "rive/viewmodel/viewmodel_instance_value.hpp"
#include "rive/viewmodel/property_symbol_dependent.hpp"
#include "rive/dirtyable.hpp"
#include "rive/enums.hpp"
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace rive
{
class Factory;
class File;
class Renderer;
class StateMachineInstance;
class TextModifierGroup;
class TextStyleBackground;
class TextStylePaint;
class TextStyleReference;
class DataContext;
class ViewModelInstanceArtboard;
class LayoutParticipant;
class TextSelectionController;
// A draw command for interleaving monochrome style paths and color glyphs.
struct TextDrawCommand
{
enum Type
{
kStylePath,
kColorGlyph
};
Type type;
TextStylePaint* style = nullptr; // for kStylePath
// For a shared style's paint: this Text's glyphs (the paint holds none).
TextStyleGeometry* geometry = nullptr;
struct ColorGlyphInfo
{
rcp<Font> font;
GlyphID glyphId;
Mat2D transform;
ColorInt foregroundColor;
float opacity;
};
ColorGlyphInfo colorGlyph; // for kColorGlyph
};
class StyledText
{
private:
/// Represents the unicode characters making up the entire text string
/// displayed. Only valid after update.
std::vector<Unichar> m_value;
std::vector<TextRun> m_runs;
public:
bool empty() const;
void clear();
void append(
rcp<Font> font,
float size,
float lineHeight,
float letterSpacing,
const std::string& text,
uint16_t styleId);
const std::vector<Unichar>& unichars() const { return m_value; }
const std::vector<TextRun>& runs() const { return m_runs; }
void swapRuns(std::vector<TextRun>& otherRuns) { m_runs.swap(otherRuns); }
};
struct TextBoundsInfo
{
float minY;
float maxWidth;
float totalHeight;
int ellipsisLine;
bool isEllipsisLineLast;
// Vertical trim removed from the top (ascent above the first line's cap- or
// x-height) and bottom (descent below the last line's baseline / natural
// descent). Both are zero when trim is disabled (TextTrimTop/Bottom::none).
float topTrim;
float bottomTrim;
};
enum class LineIter : uint8_t
{
drawLine,
skipThisLine,
yOutOfBounds
};
class Text;
#ifdef WITH_RIVE_TEXT
class TextValueRunListener;
class TextValueRunProperty : public PropertySymbolDependentSingle
{
public:
TextValueRunProperty(
Core* textValueRun,
TextValueRunListener* textValueRunListener,
ViewModelInstanceValue* instanceValue,
uint16_t propertyKey,
SymbolType symbolType);
void writeValue() override;
private:
SymbolType m_symbolType = SymbolType::none;
};
class TextValueRunListener : public CoreObjectListener
{
public:
TextValueRunListener(
TextValueRun* textValueRun,
rcp<ViewModelInstance> instance,
Text* text);
void markDirty() override;
// Points the run at the style its item's style symbols resolve to.
void resolveStyle();
Text* text() { return m_text; }
// The list item the run shows.
const rcp<ViewModelInstance>& viewModelInstance() const { return m_instance; }
TextValueRun* textValueRun()
{
if (m_core)
{
return m_core->as<TextValueRun>();
}
return nullptr;
}
protected:
void createProperties() override;
private:
Text* m_text = nullptr;
void createPropertyListener(SymbolType symbolType);
TextValueRunProperty* createSinglePropertyListener(SymbolType symbolType);
};
#endif
struct ColorGlyphCacheKey
{
const Font* font;
GlyphID glyphId;
bool operator==(const ColorGlyphCacheKey& o) const
{
return font == o.font && glyphId == o.glyphId;
}
};
struct ColorGlyphCacheHash
{
size_t operator()(const ColorGlyphCacheKey& k) const
{
size_t h = std::hash<const void*>()(k.font);
h ^= std::hash<uint16_t>()(k.glyphId) + 0x9e3779b9 + (h << 6) + (h >> 2);
return h;
}
};
// Sweep gradients have no Rive shader and fall back to their first stop.
void drawColorGlyphLayer(
Renderer* renderer,
Factory* factory,
Font::ColorGlyphLayer& layer,
float opacity);
class Text : public TextBase,
public TextInterface,
public DataBindListItemConsumer,
public AdvancingComponent,
public HostDataContextConsumer
{
public:
#ifdef WITH_RIVE_EDITOR
void editorPrepareForDestruction() override;
#endif
// Implements TextInterface
~Text();
void markShapeDirty() override;
void markPaintDirty() override;
void styleReferenceWillChange() override;
void styleReferenceDidChange() override;
// Advances the global text styles instanced on demand for list runs.
bool advanceComponent(
float elapsedSeconds,
AdvanceFlags flags = AdvanceFlags::Animate |
AdvanceFlags::NewFrame) override;
QuietState quietState() override;
// Passes the hosting artboard's data context on to the global text styles
// instanced on demand for list runs.
void hostDataContext(rcp<DataContext> value) override;
void relinkHostDataContext() override;
void updateHostedDataBinds() override;
void file(File* value) { m_file = value; }
File* file() const { return m_file; }
void draw(Renderer* renderer) override;
Core* hitTest(HitInfo*, const Mat2D&) override;
void addRun(TextValueRun* run);
void addModifierGroup(TextModifierGroup* group);
#ifdef WITH_RIVE_EDITOR
void addModifierGroupForEditor(TextModifierGroup* group);
void removeModifierGroupForEditor(TextModifierGroup* group);
/// Re-order `m_allRuns` (and `m_runs`) by the caller-supplied
/// comparator. The runtime `.riv` import preserves sibling
/// order via the exporter; coop hydration delivers cores in
/// arrival order, so the editor needs to re-sort after batch
/// finalize so concatenated text reads in FractionalIndex
/// order ("100%", not "%100").
void sortRunsForEditor(
const std::function<bool(TextValueRun*, TextValueRun*)>& cmp);
#endif
void markShapeDirty(bool sendToLayout);
void modifierShapeDirty();
void update(ComponentDirt value) override;
void onDirty(ComponentDirt value) override;
// Participates in a parent layout via an optional
// LayoutParticipant child (origin-based; text sizes via its own layout).
void composeWorldTransform() override;
protected:
void updateConstraints() override;
public:
Vec2D layoutBaseTranslation(LayoutParticipant* participant) const;
LayoutParticipant* layoutParticipant() const;
bool isParticipatingInLayout() const;
Mat2D m_transform;
Mat2D m_shapeWorldTransform;
const Mat2D& shapeWorldTransform() const { return m_shapeWorldTransform; }
TextSizing sizing() const { return (TextSizing)sizingValue(); }
TextSizing effectiveSizing() const;
TextOverflow overflow() const { return (TextOverflow)overflowValue(); }
TextOrigin textOrigin() const { return (TextOrigin)originValue(); }
TextTrimTop verticalTrimTop() const
{
return textTrimTop(verticalTrimValue());
}
TextTrimBottom verticalTrimBottom() const
{
return textTrimBottom(verticalTrimValue());
}
TextWrap wrap() const { return (TextWrap)wrapValue(); }
TextWordBreak wordBreak() const { return (TextWordBreak)wordBreakValue(); }
VerticalTextAlign verticalAlign() const
{
return (VerticalTextAlign)verticalAlignValue();
}
TextAlign align() const;
void overflow(TextOverflow value) { return overflowValue((uint8_t)value); }
void buildRenderStyles();
const TextStylePaint* styleFromShaperId(uint16_t id) const;
bool modifierRangesNeedShape() const;
// localBounds() is the LAYOUT box, not the ink. Glyphs escape it through
// negative side bearings, italic overhang, TextOverflow::visible,
// TextModifierGroup transforms, and per-TextStyle strokes and feathers. It
// is the right neighbourhood but not a bound, so it is approximate -- a
// caller sizing a surface must pad it.
BoundsFidelity paintedWorldBounds(AABB* out) override;
AABB localBounds() const override;
AABB constraintBounds() const override { return localBounds(); }
void originXChanged() override;
void originYChanged() override;
StatusCode import(ImportStack& importStack) override;
Core* clone() const override;
Vec2D measureLayout(
float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode) override;
void controlSize(
Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction) override;
float effectiveWidth()
{
return std::isnan(m_layoutWidth) ? width() : m_layoutWidth;
}
float effectiveHeight()
{
return std::isnan(m_layoutHeight) ? height() : m_layoutHeight;
}
// Whether a fitFontSize text reports its fitted size to the layout. The
// authored flag only takes effect for files built against that behavior:
// older ones were laid out against the unshrunk box and would reflow.
// See Text::import and File::minorVersion.
bool fitFontSizeResizesBoxActive() const
{
return fitFontSizeResizesBox() &&
hasFileFeature(FileFeatures::fitFontSizeResizesBox);
}
// Whether a layout owns our WIDTH axis, asked of the participant rather
// than of m_layoutWidth: that is only written by controlSize, which runs
// after the solve, so during the measure pass it is still NAN. A boxed
// width means the authored width() is not our width -- the layout's offer
// is.
bool layoutOwnsWidth() const;
// Overflow treats the box as fixed once a layout sizes our box.
bool overflowAsFixed() const
{
return effectiveSizing() == TextSizing::fixed ||
!std::isnan(layoutBoxWidth());
}
// The size a controlling layout imposes on our *box*, or NAN when it
// imposes none. Not the same as effectiveWidth/Height above, which use
// m_layoutWidth/Height whenever a layout controls us: before 7.3 the
// layout sized the text but an auto-sized box still came from the
// content, so these are NAN there. See Text::import.
float layoutBoxWidth() const
{
return hasFileFeature(FileFeatures::layoutSizesBox) ? m_layoutWidth : NAN;
}
float layoutBoxHeight() const
{
return hasFileFeature(FileFeatures::layoutSizesBox) ? m_layoutHeight : NAN;
}
float computedWidth() override { return localBounds().width(); };
float computedHeight() override { return localBounds().height(); };
void updateList(std::vector<rcp<ViewModelInstanceListItem>>* list) override;
#ifdef WITH_RIVE_TEXT
const std::vector<TextValueRun*>& runs() const { return m_allRuns; }
/// Breaks paragraphs into lines and aligns them. Lines are aligned within
/// the larger of the natural paragraph width and minAlignWidth, so a
/// caller that wants text aligned within a field wider than the text
/// (see RawTextInput::alignWidth) gets that, while text overflowing the
/// field falls back to starting at 0.
static SimpleArray<SimpleArray<GlyphLine>> BreakLines(
const SimpleArray<Paragraph>& paragraphs,
float width,
TextAlign align,
TextWrap wrap,
TextWordBreak wordBreak = TextWordBreak::breakWord,
float minAlignWidth = 0.0f);
const std::vector<TextStylePaint*>& textStylePaints()
{
return m_textStylePaints;
}
/// The style a list-driven run's item resolves to: its globalTextStyle
/// (a handle showing that style, else one instanced on demand), else its
/// textStyle name, else the first style. Null when the item has neither
/// style symbol.
TextStyleReference* resolveListRunStyle(ViewModelInstance* instance);
/// The style a run whose styleId is data bound to [value] draws: the one a
/// list run naming that style would (a handle showing it, else one
/// instanced on demand), except that a style with a state machine gets an
/// instance of its own per bound run. Null when [value] names no text
/// style.
TextStyleReference* boundRunStyle(
ViewModelInstanceArtboard* value,
const TextValueRun* run);
/// A run now draws another style: drops the on-demand styles no run draws
/// any more and re-shapes.
void runStyleChanged();
/// Whether a run of this Text may use a style that listens to its runs
/// (see TextStyleShareInfo::listensToRuns), so the state machines of
/// this Text's artboard forward pointers to those styles' state machines.
bool mayHaveListeningRuns();
/// Appends to [out] the state machines of this Text's styles that listen
/// to the runs drawn with them (see TextStyleReference::runStateMachine).
void runStateMachines(std::vector<StateMachineInstance*>& out);
#ifdef TESTING
size_t onDemandStyleCount() const { return m_onDemandStyles.size(); }
#endif
#endif
bool haveModifiers() const
{
#ifdef WITH_RIVE_TEXT
return !m_modifierGroups.empty();
#else
return false;
#endif
}
#ifdef TESTING
const std::vector<OrderedLine>& orderedLines() const
{
return m_orderedLines;
}
const std::vector<TextModifierGroup*>& modifierGroups() const
{
return m_modifierGroups;
}
const SimpleArray<Paragraph>& shape() const { return m_shape; }
const std::vector<Unichar>& unichars() const
{
return m_styledText.unichars();
}
#endif
protected:
void alignValueChanged() override;
void sizingValueChanged() override;
void overflowValueChanged() override;
void wordBreakValueChanged() override;
void widthChanged() override;
void heightChanged() override;
void paragraphSpacingChanged() override;
bool makeStyled(
StyledText& styledText,
bool withModifiers = true,
float fontScale = 1.0f) const;
void originValueChanged() override;
void verticalTrimValueChanged() override;
void fitFontSizeResizesBoxChanged() override;
private:
#ifdef WITH_RIVE_TEXT
friend class TextSelectionController;
TextSelectionController* m_selectionController = nullptr;
void updateOriginWorldTransform();
std::vector<TextValueRun*> m_runs;
std::vector<TextValueRun*> m_allRuns;
std::vector<TextStylePaint*> m_renderStyles;
std::vector<TextDrawCommand> m_drawCommands;
// This Text's glyphs for each shared global style paint it draws (the
// paint itself holds none, see TextStylePaint::isShared).
std::vector<std::pair<TextStylePaint*, std::unique_ptr<TextStyleGeometry>>>
m_sharedGeometry;
TextStyleGeometry& sharedGeometry(TextStylePaint* paint);
// Backgrounds of global styles' paints this Text used in its last build.
std::vector<TextStyleBackground*> m_globalBackgrounds;
SimpleArray<Paragraph> m_shape;
SimpleArray<Paragraph> m_modifierShape;
SimpleArray<SimpleArray<GlyphLine>> m_lines;
SimpleArray<SimpleArray<GlyphLine>> m_modifierLines;
// Runs ordered by paragraph line.
std::vector<OrderedLine> m_orderedLines;
GlyphRun m_ellipsisRun;
RawPath m_clipRect;
ShapePaintPath m_clipPath;
AABB m_bounds;
// The font-size multiplier chosen by the last fitFontSize layout. Per-run
// values are baked into the shaped runs by makeStyled; paragraph spacing is
// a Text-level gap added between paragraphs during layout, so it is scaled
// by this to keep the fitted layout a true uniform scale of the authored
// one. Only meaningful while fitFontSize is the active overflow -- read it
// through fitParagraphSpacing(), never directly, so a stale value from a
// previous fit can't leak into a paint-only rebuild.
float m_fitFontScale = 1.0f;
std::vector<TextModifierGroup*> m_modifierGroups;
StyledText m_styledText;
StyledText m_modifierStyledText;
GlyphLookup m_glyphLookup;
std::vector<TextStylePaint*> m_textStylePaints;
void clearRenderStyles();
void drawColorGlyph(
Renderer* renderer,
const TextDrawCommand::ColorGlyphInfo& info,
const Mat2D& worldTransform);
std::unordered_map<ColorGlyphCacheKey, rcp<RenderImage>, ColorGlyphCacheHash>
m_emojiImageCache;
float fitParagraphSpacing() const
{
return overflow() == TextOverflow::fitFontSize
? paragraphSpacing() * m_fitFontScale
: paragraphSpacing();
}
TextBoundsInfo computeBoundsInfo();
// For TextOverflow::fitFontSize: binary-searches the largest integer font
// size that fits the bounds and returns it as a multiplier of the authored
// font size(s). Returns 1.0f when no fitting is needed/possible.
float fitFontScale();
// Same search, against an explicit box. Used by measure(), where the box
// Yoga is offering is not yet reflected in m_layoutWidth/Height.
float fitFontScale(float boxWidth, float boxHeight);
LineIter shouldDrawLine(float y, float totalHeight, const GlyphLine& line);
void buildTextStylePaints();
std::vector<TextValueRunListener*> m_valueRunListeners;
// Global text styles instanced on demand for list runs, one per style
// (and item-supplied view model instance), shared by this Text's runs.
std::vector<std::unique_ptr<TextStyleOnDemand>> m_onDemandStyles;
// Runs may have started using a style that listens to its runs (see
// markListenedRuns); set whenever the shape is dirtied.
bool m_listenedRunsDirty = true;
/// Makes every run drawn with a style that listens to its runs a hit
/// target, so the style's state machine can hit-test it.
void markListenedRuns();
// A style may now be drawn for one list run alone, or no longer (see
// syncStyleListItems); set whenever the shape is dirtied.
bool m_listItemsDirty = true;
/// Gives each global style drawn for one list run alone that run's list
/// item, so the style reads it (see TextStyleInstance::listItem). A style
/// several runs share gets none.
void syncStyleListItems();
/// The style a list run showing [item] draws when its item names the
/// style [value] holds (see resolveListRunStyle).
TextStyleReference* globalStyleReference(
ViewModelInstanceArtboard* value,
ViewModelInstance* item,
const TextValueRun* owner = nullptr);
TextStyleReference* styleReferenceNamed(const std::string& name);
TextStyleReference* firstStyleReference();
void resolveListRunStyles();
void pruneOnDemandStyles();
// What measure() last reported to the layout and what update() last
// shaped, as measureKey hashes. Equal means the layout has already sized
// the text as it is now shaped, so rebuilding it owes the layout nothing.
// They start unequal: a text the layout never measured keeps notifying it.
uint64_t m_measuredKey = 0;
uint64_t m_shapedKey = 1;
uint64_t measureKey(const StyledText& styled, float fontScale) const;
#endif
File* m_file = nullptr;
float m_layoutWidth = NAN;
float m_layoutHeight = NAN;
uint8_t m_layoutWidthScaleType = std::numeric_limits<uint8_t>::max();
uint8_t m_layoutHeightScaleType = std::numeric_limits<uint8_t>::max();
LayoutDirection m_layoutDirection = LayoutDirection::inherit;
// Behaviors the file's version opts into, stamped in Text::import from
// the header. Each one guards a change that would relayout content
// authored before it, so a file predating the change keeps the old
// behavior; anything built in memory is current and gets them all.
//
// One mask rather than a bool apiece because they are all the same shape
// -- stamp at import, carry through clone, read at a single call site --
// so the next gate costs a bit instead of a member plus its plumbing.
// See File::minorVersion for what each version changed.
enum class FileFeatures : uint8_t
{
none = 0,
// 7.3: a controlling layout sizes our box, not just our text. Below
// this an auto-sized text still took its box from the content, so the
// box could disagree with the slot and every overflow mode was inert.
layoutSizesBox = 1 << 0,
// 7.4: Text::fitFontSizeResizesBox is honored, so a fitFontSize text
// reports its *fitted* size to the layout instead of reserving room
// at the authored font size.
fitFontSizeResizesBox = 1 << 1,
all = layoutSizesBox | fitFontSizeResizesBox,
};
FileFeatures m_fileFeatures = FileFeatures::all;
bool hasFileFeature(FileFeatures feature) const
{
return enums::is_flag_set(m_fileFeatures, feature);
}
Vec2D measure(Vec2D maxSize);
};
} // namespace rive
#endif