A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

788 lines
26 KiB

using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
/// <summary>
/// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
/// </summary>
public sealed class GlyphRun : IDisposable
{
private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();
private IGlyphRunImpl? _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
private int _biDiLevel;
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private int _offsetToFirstCharacter;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="biDiLevel">The bidi level.</param>
public GlyphRun(
GlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0)
{
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
Characters = characters;
_glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
GlyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
}
/// <summary>
/// Gets the <see cref="Media.GlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary>
public GlyphTypeface GlyphTypeface => _glyphTypeface;
/// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
/// </summary>
public double FontRenderingEmSize
{
get => _fontRenderingEmSize;
set => Set(ref _fontRenderingEmSize, value);
}
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
/// <summary>
///
/// </summary>
public GlyphRunMetrics Metrics
{
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
/// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary>
public Point BaselineOrigin
{
get
{
_baselineOrigin ??= CalculateBaselineOrigin();
return _baselineOrigin.Value;
}
set => Set(ref _baselineOrigin, value);
}
/// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary>
public IReadOnlyList<ushort> GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
}
/// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary>
public IReadOnlyList<double>? GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
}
/// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<Vector>? GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
}
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
}
/// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
/// </summary>
public IReadOnlyList<int>? GlyphClusters
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
}
/// <summary>
/// Gets or sets the bidirectional nesting level of the <see cref="GlyphRun"/>.
/// </summary>
public int BiDiLevel
{
get => _biDiLevel;
set => Set(ref _biDiLevel, value);
}
/// <summary>
/// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
/// <summary>
/// Returns <c>true</c> if the text direction is left-to-right. Otherwise, returns <c>false</c>.
/// </summary>
public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
/// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
public IGlyphRunImpl GlyphRunImpl
{
get
{
if (_glyphRunImpl == null)
{
Initialize();
}
return _glyphRunImpl!;
}
}
/// <summary>
/// Obtains geometry for the glyph run.
/// </summary>
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
public Geometry BuildGeometry()
{
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
return new PlatformGeometry(geometryImpl);
}
/// <summary>
/// Retrieves the offset from the leading edge of the <see cref="GlyphRun"/>
/// to the leading or trailing edge of a caret stop containing the specified character hit.
/// </summary>
/// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the offset.</param>
/// <returns>
/// A <see cref="double"/> that represents the offset from the leading edge of the <see cref="GlyphRun"/>
/// to the leading or trailing edge of a caret stop containing the character hit.
/// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
var distance = 0.0;
if (IsLeftToRight)
{
if (GlyphClusters != null)
{
if (characterIndex < GlyphClusters[0])
{
return 0;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
return Metrics.WidthIncludingTrailingWhitespace;
}
}
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
//Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0)
{
while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
{
glyphIndex++;
}
}
}
for (var i = 0; i < glyphIndex; i++)
{
distance += GetGlyphAdvance(i, out _);
}
return distance;
}
else
{
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
if (characterIndex > GlyphClusters[0])
{
return 0;
}
if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
{
return Size.Width;
}
}
for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
{
distance += GetGlyphAdvance(i, out _);
}
return Size.Width - distance;
}
}
/// <summary>
/// Retrieves the <see cref="CharacterHit"/> value that represents the character hit of the caret of the <see cref="GlyphRun"/>.
/// </summary>
/// <param name="distance">Offset to use for computing the caret character hit.</param>
/// <param name="isInside">Determines whether the character hit is inside the <see cref="GlyphRun"/>.</param>
/// <returns>
/// A <see cref="CharacterHit"/> value that represents the character hit that is closest to the distance value.
/// The out parameter <c>isInside</c> returns <c>true</c> if the character hit is inside the <see cref="GlyphRun"/>; otherwise, <c>false</c>.
/// </returns>
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
//After
if (distance >= Size.Width)
{
isInside = false;
characterIndex = GlyphIndices.Count - 1;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
//Within
var currentX = 0d;
if (IsLeftToRight)
{
for (var index = 0; index < GlyphIndices.Count; index++)
{
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (distance > currentX && distance <= currentX + advance)
{
break;
}
currentX += advance;
}
}
else
{
currentX = Size.Width;
for (var index = GlyphIndices.Count - 1; index >= 0; index--)
{
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (currentX - advance < distance)
{
break;
}
currentX -= advance;
}
}
isInside = true;
var characterHit = FindNearestCharacterHit(characterIndex, out var width);
var delta = width / 2;
var offset = IsLeftToRight ? distance - currentX : currentX - distance;
var isTrailing = offset > delta;
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
}
/// <summary>
/// Retrieves the next valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
/// </summary>
/// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the next hit value.</param>
/// <returns>
/// A <see cref="CharacterHit"/> that represents the next valid caret character hit in the logical direction.
/// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
/// </returns>
public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (characterHit.TrailingLength == 0)
{
characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return textPosition > _characters.End ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
if (characterHit == nextCharacterHit)
{
return characterHit;
}
return characterHit.TrailingLength > 0 ?
nextCharacterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
/// <summary>
/// Retrieves the previous valid caret character hit in the logical direction in the <see cref="GlyphRun"/>.
/// </summary>
/// <param name="characterHit">The <see cref="CharacterHit"/> to use for computing the previous hit value.</param>
/// <returns>
/// A cref="CharacterHit"/> that represents the previous valid caret character hit in the logical direction.
/// If the return value is equal to <c>characterHit</c>, no further navigation is possible in the <see cref="GlyphRun"/>.
/// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
if (characterHit.TrailingLength != 0)
{
return new CharacterHit(characterHit.FirstCharacterIndex);
}
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
}
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
/// <param name="characterIndex">The character index.</param>
/// <returns>
/// The glyph index.
/// </returns>
public int FindGlyphIndex(int characterIndex)
{
if (GlyphClusters == null)
{
return characterIndex;
}
if (IsLeftToRight)
{
if (characterIndex < GlyphClusters[0])
{
return 0;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
}
else
{
if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
if (characterIndex > GlyphClusters[0])
{
return 0;
}
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = GlyphClusters;
// Find the start of the cluster at the character index.
var start = clusters.BinarySearch(characterIndex, comparer);
// No cluster found.
if (start < 0)
{
while (characterIndex > 0 && start < 0)
{
characterIndex--;
start = clusters.BinarySearch(characterIndex, comparer);
}
if (start < 0)
{
return -1;
}
}
if (IsLeftToRight)
{
while (start > 0 && clusters[start - 1] == clusters[start])
{
start--;
}
}
else
{
while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
{
start++;
}
}
return start;
}
/// <summary>
/// Finds the nearest <see cref="CharacterHit"/> at given index.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="width">The width of found cluster.</param>
/// <returns>
/// The nearest <see cref="CharacterHit"/>.
/// </returns>
public CharacterHit FindNearestCharacterHit(int index, out double width)
{
width = 0.0;
var start = FindGlyphIndex(index);
if (GlyphClusters == null)
{
width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1);
}
var cluster = GlyphClusters[start];
var nextCluster = cluster;
var currentIndex = start;
while (nextCluster == cluster)
{
width += GetGlyphAdvance(currentIndex, out _);
if (IsLeftToRight)
{
currentIndex++;
if (currentIndex == GlyphClusters.Count)
{
break;
}
}
else
{
currentIndex--;
if (currentIndex < 0)
{
break;
}
}
nextCluster = GlyphClusters[currentIndex];
}
int trailingLength;
if (nextCluster == cluster)
{
trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
{
trailingLength = nextCluster - cluster;
}
return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
}
/// <summary>
/// Gets a glyph's width.
/// </summary>
/// <param name="index">The glyph index.</param>
/// <param name="cluster">The current cluster.</param>
/// <returns>The glyph's width.</returns>
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
}
var glyph = GlyphIndices[index];
return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
/// <summary>
/// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
/// </summary>
/// <returns>The baseline origin.</returns>
private Point CalculateBaselineOrigin()
{
return new Point(0, -GlyphTypeface.Ascent * Scale);
}
private GlyphRunMetrics CreateGlyphRunMetrics()
{
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
var firstCluster = GlyphClusters[0];
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
}
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
{
var advance = GetGlyphAdvance(index, out _);
widthIncludingTrailingWhitespace += advance;
}
var width = widthIncludingTrailingWhitespace;
if (IsLeftToRight)
{
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
}
}
else
{
for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
}
}
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height);
}
private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount)
{
glyphCount = 0;
newLineLength = 0;
if (Characters.IsEmpty)
{
return 0;
}
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
{
for (var i = _characters.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count;
glyphCount++;
}
}
else
{
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
while(i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if(currentCluster == nextCluster)
{
clusterLength++;
i--;
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
return trailingWhitespaceLength;
}
private void Set<T>(ref T field, T value)
{
_glyphRunImpl?.Dispose();
_glyphRunImpl = null;
_glyphRunMetrics = null;
_baselineOrigin = null;
field = value;
}
/// <summary>
/// Initializes the <see cref="GlyphRun"/>.
/// </summary>
private void Initialize()
{
if (GlyphIndices == null)
{
throw new InvalidOperationException();
}
var glyphCount = GlyphIndices.Count;
if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
{
throw new InvalidOperationException();
}
if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
{
throw new InvalidOperationException();
}
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this);
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
}
}