// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; using System; using System.Collections.Generic; namespace SixLabors.Fonts { /// /// Visitor types for streaming laid-out glyphs into operations. /// public sealed partial class TextBlock { /// /// Adds a flushed grapheme to its source-order word-boundary segment. /// /// The source-order word-boundary segments. /// The word metrics array being accumulated. /// The grapheme metrics just emitted. private static void AccumulateWordMetrics( List wordSegments, WordMetrics[] wordMetrics, in GraphemeMetrics grapheme) => AccumulateWordMetrics( wordSegments, wordMetrics, grapheme.GraphemeIndex, grapheme.Advance, grapheme.Bounds, grapheme.RenderableBounds); /// /// Adds one coalesced grapheme rectangle set to its source-order word-boundary segment. /// /// The source-order word-boundary segments. /// The word metrics array being accumulated. /// The source grapheme index owning the rectangles. /// The positioned logical advance rectangle for the grapheme. /// The rendered glyph bounds for the grapheme. /// The union of logical advance and rendered glyph bounds for the grapheme. private static void AccumulateWordMetrics( List wordSegments, WordMetrics[] wordMetrics, int graphemeIndex, FontRectangle advance, FontRectangle bounds, FontRectangle renderableBounds) { int wordIndex = FindWordMetricIndex(wordSegments, graphemeIndex); WordSegmentRun segment = wordSegments[wordIndex]; WordMetrics metrics = wordMetrics[wordIndex]; // WordMetrics is the final value type, but this array slot also acts as the running // accumulator for its source segment. Once a slot has source ranges, subsequent // graphemes in the same word segment union into the rectangles already stored there. bool hasMetrics = HasWordMetrics(metrics); wordMetrics[wordIndex] = new WordMetrics( hasMetrics ? FontRectangle.Union(metrics.Advance, advance) : advance, hasMetrics ? FontRectangle.Union(metrics.Bounds, bounds) : bounds, hasMetrics ? FontRectangle.Union(metrics.RenderableBounds, renderableBounds) : renderableBounds, segment.GraphemeStart, segment.GraphemeEnd, segment.StringStart, segment.StringEnd); } /// /// Coalesces consecutive laid-out glyph entries that belong to the same grapheme. /// private struct GraphemeMetricsAccumulator { private readonly GraphemeMetrics[] graphemes; private readonly float dpi; private int count; private int graphemeIndex; private int stringIndex; private int bidiLevel; private bool isLineBreak; private Font? font; private FontRectangle advanceBounds; private FontRectangle bounds; private bool hasCurrent; /// /// Initializes a new instance of the struct. /// /// The target grapheme array to fill. /// The target DPI. public GraphemeMetricsAccumulator(GraphemeMetrics[] graphemes, float dpi) { this.graphemes = graphemes; this.dpi = dpi; this.count = 0; this.graphemeIndex = 0; this.stringIndex = 0; this.bidiLevel = 0; this.isLineBreak = false; this.font = null; this.advanceBounds = FontRectangle.Empty; this.bounds = FontRectangle.Empty; this.hasCurrent = false; } /// /// Gets the number of graphemes emitted so far. /// public readonly int Count => this.count; /// /// Adds one laid-out glyph entry to the current grapheme, flushing the previous grapheme when needed. /// /// The laid-out glyph entry. public void Visit(in GlyphLayout glyph) => this.Visit(glyph, out _); /// /// Adds one laid-out glyph entry to the current grapheme, returning emitted metrics when the previous grapheme is flushed. /// /// The laid-out glyph entry. /// The emitted grapheme metrics when this method returns . /// when a grapheme was emitted. public bool Visit( in GlyphLayout glyph, out GraphemeMetrics metrics) { FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); FontRectangle bounds = glyph.MeasureBounds(this.dpi); if (!this.hasCurrent) { this.Start(glyph, advanceBounds, bounds); metrics = default; return false; } if (glyph.GraphemeIndex != this.graphemeIndex) { bool emitted = this.Flush(out metrics); this.Start(glyph, advanceBounds, bounds); return emitted; } this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); this.bounds = FontRectangle.Union(this.bounds, bounds); this.isLineBreak |= CodePoint.IsNewLine(glyph.CodePoint); metrics = default; return false; } /// /// Flushes the current line's pending grapheme. /// public void EndLine() => this.Flush(out _); /// /// Flushes the current line's pending grapheme. /// /// The emitted grapheme metrics when this method returns . /// when a grapheme was emitted. public bool EndLine(out GraphemeMetrics metrics) => this.Flush(out metrics); /// /// Starts a new grapheme from the first emitted glyph in a consecutive grapheme run. /// /// The first glyph in the grapheme. /// The positioned logical advance bounds for . /// The rendered bounds for . private void Start( in GlyphLayout glyph, in FontRectangle advanceBounds, in FontRectangle bounds) { this.graphemeIndex = glyph.GraphemeIndex; this.stringIndex = glyph.StringIndex; this.bidiLevel = glyph.BidiLevel; this.isLineBreak = CodePoint.IsNewLine(glyph.CodePoint); this.font = glyph.Font; this.advanceBounds = advanceBounds; this.bounds = bounds; this.hasCurrent = true; } /// /// Emits the current grapheme while preserving the visual order produced by text layout. /// /// The emitted grapheme metrics when this method returns . /// when a grapheme was emitted. private bool Flush(out GraphemeMetrics metrics) { if (!this.hasCurrent) { metrics = default; return false; } FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); metrics = new GraphemeMetrics( this.advanceBounds, this.bounds, renderableBounds, this.font!, this.graphemeIndex, this.stringIndex, this.bidiLevel, this.isLineBreak); this.graphemes[this.count] = metrics; this.count++; this.hasCurrent = false; return true; } } /// /// Coalesces laid-out glyph entries into grapheme metrics and word metrics in the same stream. /// private struct GraphemeAndWordMetricsAccumulator { private readonly List wordSegments; private readonly WordMetrics[] wordMetrics; private GraphemeMetricsAccumulator graphemes; /// /// Initializes a new instance of the struct. /// /// The target grapheme array to fill. /// The target DPI. /// The source-order word-boundary segments. /// The target word metrics array to fill. public GraphemeAndWordMetricsAccumulator( GraphemeMetrics[] graphemes, float dpi, List wordSegments, WordMetrics[] wordMetrics) { this.wordSegments = wordSegments; this.wordMetrics = wordMetrics; this.graphemes = new(graphemes, dpi); } /// /// Gets the number of graphemes emitted so far. /// public readonly int Count => this.graphemes.Count; /// /// Adds one laid-out glyph entry to the current grapheme and updates word metrics when a grapheme is emitted. /// /// The laid-out glyph entry. public void Visit(in GlyphLayout glyph) { if (this.graphemes.Visit(glyph, out GraphemeMetrics metrics)) { AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); } } /// /// Flushes the current line's pending grapheme and updates word metrics when a grapheme is emitted. /// public void EndLine() { if (this.graphemes.EndLine(out GraphemeMetrics metrics)) { AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); } } } /// /// Coalesces laid-out glyph entries into word metrics without storing grapheme metrics. /// private struct WordMetricsVisitor : TextLayout.IGlyphLayoutVisitor { private readonly List wordSegments; private readonly WordMetrics[] wordMetrics; private readonly float dpi; private int graphemeIndex; private FontRectangle advanceBounds; private FontRectangle bounds; private bool hasCurrent; /// /// Initializes a new instance of the struct. /// /// The source-order word-boundary segments. /// The target word metrics array to fill. /// The target DPI. public WordMetricsVisitor( List wordSegments, WordMetrics[] wordMetrics, float dpi) { this.wordSegments = wordSegments; this.wordMetrics = wordMetrics; this.dpi = dpi; this.graphemeIndex = 0; this.advanceBounds = FontRectangle.Empty; this.bounds = FontRectangle.Empty; this.hasCurrent = false; } /// public readonly void BeginLine(int lineIndex) { } /// public void Visit(in GlyphLayout glyph) { FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); FontRectangle bounds = glyph.MeasureBounds(this.dpi); if (!this.hasCurrent) { this.Start(glyph, advanceBounds, bounds); return; } if (glyph.GraphemeIndex != this.graphemeIndex) { this.Flush(); this.Start(glyph, advanceBounds, bounds); return; } this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); this.bounds = FontRectangle.Union(this.bounds, bounds); } /// public void EndLine() => this.Flush(); /// /// Starts a new word-metrics grapheme from the first emitted glyph in a consecutive grapheme run. /// /// The first glyph in the grapheme. /// The positioned logical advance bounds for . /// The rendered bounds for . private void Start( in GlyphLayout glyph, in FontRectangle advanceBounds, in FontRectangle bounds) { this.graphemeIndex = glyph.GraphemeIndex; this.advanceBounds = advanceBounds; this.bounds = bounds; this.hasCurrent = true; } /// /// Emits the current grapheme directly into its source-order word-boundary segment. /// private void Flush() { if (!this.hasCurrent) { return; } FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); AccumulateWordMetrics( this.wordSegments, this.wordMetrics, this.graphemeIndex, this.advanceBounds, this.bounds, renderableBounds); this.hasCurrent = false; } } /// /// Accumulates the rendered rectangle as glyphs stream from layout. /// private struct RenderedRectangleAccumulator : TextLayout.IGlyphLayoutVisitor { private readonly float dpi; private float left; private float top; private float right; private float bottom; private bool any; /// /// Initializes a new instance of the struct. /// /// The target DPI. public RenderedRectangleAccumulator(float dpi) { this.dpi = dpi; this.left = float.MaxValue; this.top = float.MaxValue; this.right = float.MinValue; this.bottom = float.MinValue; this.any = false; } /// public readonly void BeginLine(int lineIndex) { } /// public void Visit(in GlyphLayout glyph) { FontRectangle box = glyph.MeasureBounds(this.dpi); if (box.Width <= 0 && box.Height <= 0) { return; } if (box.Left < this.left) { this.left = box.Left; } if (box.Top < this.top) { this.top = box.Top; } if (box.Right > this.right) { this.right = box.Right; } if (box.Bottom > this.bottom) { this.bottom = box.Bottom; } this.any = true; } /// /// Returns the accumulated rendered bounds. /// /// The rendered bounds of all visited glyphs. public readonly FontRectangle Result() => this.any ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; /// public readonly void EndLine() { } } /// /// Builds the bounds and grapheme metrics array while glyphs stream from layout. /// private struct GraphemeMetricsVisitor : TextLayout.IGlyphLayoutVisitor { private readonly float dpi; private GraphemeMetricsAccumulator graphemes; private float left; private float top; private float right; private float bottom; private bool hasBounds; /// /// Initializes a new instance of the struct. /// /// The target DPI. /// The grapheme metrics array to fill. public GraphemeMetricsVisitor( float dpi, GraphemeMetrics[] graphemes) { this.dpi = dpi; this.graphemes = new(graphemes, dpi); this.left = float.MaxValue; this.top = float.MaxValue; this.right = float.MinValue; this.bottom = float.MinValue; this.hasBounds = false; } /// public readonly void BeginLine(int lineIndex) { } /// public void Visit(in GlyphLayout glyph) { FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; if (hasGlyphBox && glyphBox.Left < this.left) { this.left = glyphBox.Left; } if (hasGlyphBox && glyphBox.Top < this.top) { this.top = glyphBox.Top; } if (hasGlyphBox && glyphBox.Right > this.right) { this.right = glyphBox.Right; } if (hasGlyphBox && glyphBox.Bottom > this.bottom) { this.bottom = glyphBox.Bottom; } this.hasBounds |= hasGlyphBox; this.graphemes.Visit(glyph); } /// /// Returns the accumulated rendered bounds. /// /// The rendered bounds of all visited glyphs. public readonly FontRectangle Bounds() => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; /// public void EndLine() => this.graphemes.EndLine(); } /// /// Builds the bounds, grapheme metrics, and word metrics arrays while glyphs stream from layout. /// private struct GraphemeAndWordMetricsVisitor : TextLayout.IGlyphLayoutVisitor { private readonly float dpi; private GraphemeAndWordMetricsAccumulator graphemes; private float left; private float top; private float right; private float bottom; private bool hasBounds; /// /// Initializes a new instance of the struct. /// /// The target DPI. /// The grapheme metrics array to fill. /// The source-order word-boundary segments. /// The word metrics array to fill. public GraphemeAndWordMetricsVisitor( float dpi, GraphemeMetrics[] graphemes, List wordSegments, WordMetrics[] wordMetrics) { this.dpi = dpi; this.graphemes = new(graphemes, dpi, wordSegments, wordMetrics); this.left = float.MaxValue; this.top = float.MaxValue; this.right = float.MinValue; this.bottom = float.MinValue; this.hasBounds = false; } /// public readonly void BeginLine(int lineIndex) { } /// public void Visit(in GlyphLayout glyph) { FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; if (hasGlyphBox && glyphBox.Left < this.left) { this.left = glyphBox.Left; } if (hasGlyphBox && glyphBox.Top < this.top) { this.top = glyphBox.Top; } if (hasGlyphBox && glyphBox.Right > this.right) { this.right = glyphBox.Right; } if (hasGlyphBox && glyphBox.Bottom > this.bottom) { this.bottom = glyphBox.Bottom; } this.hasBounds |= hasGlyphBox; this.graphemes.Visit(glyph); } /// /// Returns the accumulated rendered bounds. /// /// The rendered bounds of all visited glyphs. public readonly FontRectangle Bounds() => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; /// public void EndLine() => this.graphemes.EndLine(); } /// /// Builds the per-line grapheme metrics results while glyphs stream from layout. /// private struct LineLayoutVisitor : TextLayout.IGlyphLayoutVisitor { private readonly TextBox textBox; private readonly TextOptions options; private readonly float wrappingLength; private readonly LineMetrics[] metrics; private readonly LineLayout[] lines; private readonly GraphemeMetrics[] graphemes; private readonly WordMetrics[] wordMetrics; private GraphemeAndWordMetricsAccumulator graphemeAccumulator; private int lineIndex; private int lineGraphemeStart; private int metricIndex; /// /// Initializes a new instance of the struct. /// /// The shaped and line-broken text box. /// The text options used for layout. /// The wrapping length in pixels. /// The grapheme metrics array to fill. /// The line metrics aligned with the line-broken text box. /// The line layout array to fill. /// The source-order word-boundary segments. /// The word metrics for the source text. /// The target DPI. public LineLayoutVisitor( TextBox textBox, TextOptions options, float wrappingLength, GraphemeMetrics[] graphemes, LineMetrics[] metrics, LineLayout[] lines, List wordSegments, WordMetrics[] wordMetrics, float dpi) { this.textBox = textBox; this.options = options; this.wrappingLength = wrappingLength; this.metrics = metrics; this.lines = lines; this.graphemes = graphemes; this.wordMetrics = wordMetrics; this.graphemeAccumulator = new(graphemes, dpi, wordSegments, wordMetrics); this.lineIndex = 0; this.lineGraphemeStart = 0; this.metricIndex = 0; } /// public void BeginLine(int lineIndex) { this.lineGraphemeStart = this.graphemeAccumulator.Count; this.metricIndex = lineIndex; } /// public void Visit(in GlyphLayout glyph) => this.graphemeAccumulator.Visit(glyph); /// public void EndLine() { this.graphemeAccumulator.EndLine(); // TextLayout owns the visual line loop, so the slice is recorded here instead of // reconstructing line membership from metrics after glyph emission. ReadOnlyMemory lineGraphemes = new(this.graphemes, this.lineGraphemeStart, this.graphemeAccumulator.Count - this.lineGraphemeStart); this.lines[this.lineIndex] = new LineLayout( this.textBox, this.options, this.wrappingLength, this.metricIndex, in this.metrics[this.metricIndex], lineGraphemes, this.wordMetrics); this.lineIndex++; } } /// /// Builds one per-glyph metrics array while glyphs stream from layout. /// private struct GlyphMetricsVisitor : TextLayout.IGlyphLayoutVisitor { private readonly GlyphMetrics[] glyphMetrics; private readonly float dpi; private readonly int lineIndex; private int count; private int currentLineIndex; /// /// Initializes a new instance of the struct. /// /// The target array to fill. /// The target DPI. public GlyphMetricsVisitor( GlyphMetrics[] glyphMetrics, float dpi) : this(glyphMetrics, dpi, -1) { } /// /// Initializes a new instance of the struct. /// /// The target array to fill. /// The target DPI. /// The line index to collect. public GlyphMetricsVisitor( GlyphMetrics[] glyphMetrics, float dpi, int lineIndex) { this.glyphMetrics = glyphMetrics; this.dpi = dpi; this.lineIndex = lineIndex; this.count = 0; this.currentLineIndex = -1; } /// public void BeginLine(int lineIndex) => this.currentLineIndex = lineIndex; /// public void Visit(in GlyphLayout glyph) { if (this.lineIndex >= 0 && this.currentLineIndex != this.lineIndex) { return; } FontRectangle advance = glyph.MeasureAdvance(this.dpi); FontRectangle bounds = glyph.MeasureBounds(this.dpi); FontRectangle renderableBounds = FontRectangle.Union(advance, bounds); this.glyphMetrics[this.count] = new GlyphMetrics( glyph.Glyph.GlyphMetrics.CodePoint, advance, bounds, renderableBounds, glyph.Font, glyph.GraphemeIndex, glyph.StringIndex); this.count++; } /// public readonly void EndLine() { } } /// /// Renders glyphs as they stream from layout. /// private struct GlyphRendererVisitor : TextLayout.IGlyphLayoutVisitor { private readonly IGlyphRenderer renderer; private readonly TextOptions options; private readonly int lineIndex; private int currentLineIndex; /// /// Initializes a new instance of the struct. /// /// The target renderer. /// The text options used for rendering. /// The line index to render, or -1 to render every line. public GlyphRendererVisitor(IGlyphRenderer renderer, TextOptions options, int lineIndex) { this.renderer = renderer; this.options = options; this.lineIndex = lineIndex; this.currentLineIndex = -1; } /// public void BeginLine(int lineIndex) => this.currentLineIndex = lineIndex; /// public readonly void Visit(in GlyphLayout glyph) { if (this.lineIndex > -1 && this.currentLineIndex != this.lineIndex) { return; } glyph.Glyph.RenderTo(this.renderer, glyph.GraphemeIndex, glyph.GlyphOrigin, glyph.DecorationOrigin, glyph.LayoutMode, this.options); } /// public readonly void EndLine() { } } } }