29 changed files with 1389 additions and 7 deletions
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="HarfBuzzSharp" Version="2.6.1-rc.153" /> |
<PackageReference Include="HarfBuzzSharp" Version="2.6.1" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1-rc.153" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" /> |
<PackageReference Include="SkiaSharp" Version="1.68.1" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1-rc.153" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -0,0 +1,14 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
x:Class="RenderDemo.Pages.GlyphRunPage"> |
||||
|
<Border |
||||
|
Background="White"> |
||||
|
<DrawingPresenter |
||||
|
x:Name="drawingPresenter" |
||||
|
Stretch="None"> |
||||
|
</DrawingPresenter> |
||||
|
</Border> |
||||
|
</UserControl> |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace RenderDemo.Pages |
||||
|
{ |
||||
|
public class GlyphRunPage : UserControl |
||||
|
{ |
||||
|
private DrawingPresenter _drawingPresenter; |
||||
|
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; |
||||
|
private readonly Random _rand = new Random(); |
||||
|
private ushort[] _glyphIndices = new ushort[1]; |
||||
|
private float _fontSize = 20; |
||||
|
private int _direction = 10; |
||||
|
|
||||
|
public GlyphRunPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
|
||||
|
_drawingPresenter = this.FindControl<DrawingPresenter>("drawingPresenter"); |
||||
|
|
||||
|
DispatcherTimer.Run(() => |
||||
|
{ |
||||
|
UpdateGlyphRun(); |
||||
|
|
||||
|
return true; |
||||
|
}, TimeSpan.FromSeconds(1)); |
||||
|
} |
||||
|
|
||||
|
private void UpdateGlyphRun() |
||||
|
{ |
||||
|
var c = (uint)_rand.Next(65, 90); |
||||
|
|
||||
|
if (_fontSize + _direction > 200) |
||||
|
{ |
||||
|
_direction = -10; |
||||
|
} |
||||
|
|
||||
|
if (_fontSize + _direction < 20) |
||||
|
{ |
||||
|
_direction = 10; |
||||
|
} |
||||
|
|
||||
|
_fontSize += _direction; |
||||
|
|
||||
|
_glyphIndices[0] = _glyphTypeface.GetGlyph(c); |
||||
|
|
||||
|
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; |
||||
|
|
||||
|
var drawingGroup = new DrawingGroup(); |
||||
|
|
||||
|
var glyphRunDrawing = new GlyphRunDrawing |
||||
|
{ |
||||
|
Foreground = Brushes.Black, |
||||
|
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), |
||||
|
BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) |
||||
|
}; |
||||
|
|
||||
|
drawingGroup.Children.Add(glyphRunDrawing); |
||||
|
|
||||
|
var geometryDrawing = new GeometryDrawing |
||||
|
{ |
||||
|
Pen = new Pen(Brushes.Black), |
||||
|
Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } |
||||
|
}; |
||||
|
|
||||
|
drawingGroup.Children.Add(geometryDrawing); |
||||
|
|
||||
|
_drawingPresenter.Drawing = drawingGroup; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents information about a character hit within a glyph run.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The CharacterHit structure provides information about the index of the first
|
||||
|
/// character that got hit as well as information about leading or trailing edge.
|
||||
|
/// </remarks>
|
||||
|
public readonly struct CharacterHit : IEquatable<CharacterHit> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CharacterHit"/> structure.
|
||||
|
/// </summary>
|
||||
|
/// <param name="firstCharacterIndex">Index of the first character that got hit.</param>
|
||||
|
/// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge,
|
||||
|
/// this value is the number of code points until the next valid caret position.</param>
|
||||
|
public CharacterHit(int firstCharacterIndex, int trailingLength = 0) |
||||
|
{ |
||||
|
FirstCharacterIndex = firstCharacterIndex; |
||||
|
|
||||
|
TrailingLength = trailingLength; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the index of the first character that got hit.
|
||||
|
/// </summary>
|
||||
|
public int FirstCharacterIndex { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the trailing length value for the character that got hit.
|
||||
|
/// </summary>
|
||||
|
public int TrailingLength { get; } |
||||
|
|
||||
|
public bool Equals(CharacterHit other) |
||||
|
{ |
||||
|
return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength; |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
return obj is CharacterHit other && Equals(other); |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
unchecked |
||||
|
{ |
||||
|
return FirstCharacterIndex * 397 ^ TrailingLength; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static bool operator ==(CharacterHit left, CharacterHit right) |
||||
|
{ |
||||
|
return left.Equals(right); |
||||
|
} |
||||
|
|
||||
|
public static bool operator !=(CharacterHit left, CharacterHit right) |
||||
|
{ |
||||
|
return !left.Equals(right); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,459 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utility; |
||||
|
|
||||
|
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 IPlatformRenderInterface s_platformRenderInterface = |
||||
|
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
||||
|
|
||||
|
private IGlyphRunImpl _glyphRunImpl; |
||||
|
private GlyphTypeface _glyphTypeface; |
||||
|
private double _fontRenderingEmSize; |
||||
|
private Rect? _bounds; |
||||
|
|
||||
|
private ReadOnlySlice<ushort> _glyphIndices; |
||||
|
private ReadOnlySlice<double> _glyphAdvances; |
||||
|
private ReadOnlySlice<Vector> _glyphOffsets; |
||||
|
private ReadOnlySlice<ushort> _glyphClusters; |
||||
|
private ReadOnlySlice<char> _characters; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="GlyphRun"/> class.
|
||||
|
/// </summary>
|
||||
|
public GlyphRun() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/// <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>
|
||||
|
/// <param name="bounds">The bound.</param>
|
||||
|
public GlyphRun( |
||||
|
GlyphTypeface glyphTypeface, |
||||
|
double fontRenderingEmSize, |
||||
|
ReadOnlySlice<ushort> glyphIndices, |
||||
|
ReadOnlySlice<double> glyphAdvances = default, |
||||
|
ReadOnlySlice<Vector> glyphOffsets = default, |
||||
|
ReadOnlySlice<char> characters = default, |
||||
|
ReadOnlySlice<ushort> glyphClusters = default, |
||||
|
int bidiLevel = 0, |
||||
|
Rect? bounds = null) |
||||
|
{ |
||||
|
GlyphTypeface = glyphTypeface; |
||||
|
|
||||
|
FontRenderingEmSize = fontRenderingEmSize; |
||||
|
|
||||
|
GlyphIndices = glyphIndices; |
||||
|
|
||||
|
GlyphAdvances = glyphAdvances; |
||||
|
|
||||
|
GlyphOffsets = glyphOffsets; |
||||
|
|
||||
|
Characters = characters; |
||||
|
|
||||
|
GlyphClusters = glyphClusters; |
||||
|
|
||||
|
BidiLevel = bidiLevel; |
||||
|
|
||||
|
Initialize(bounds); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the <see cref="Media.GlyphTypeface"/> for the <see cref="GlyphRun"/>.
|
||||
|
/// </summary>
|
||||
|
public GlyphTypeface GlyphTypeface |
||||
|
{ |
||||
|
get => _glyphTypeface; |
||||
|
set => Set(ref _glyphTypeface, value); |
||||
|
} |
||||
|
|
||||
|
/// <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 an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
|
||||
|
/// </summary>
|
||||
|
public ReadOnlySlice<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 ReadOnlySlice<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 ReadOnlySlice<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 ReadOnlySlice<ushort> 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; |
||||
|
set; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
///
|
||||
|
/// </summary>
|
||||
|
internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight; |
||||
|
|
||||
|
/// <summary>
|
||||
|
///
|
||||
|
/// </summary>
|
||||
|
internal bool IsLeftToRight => ((BidiLevel & 1) == 0); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
|
||||
|
/// </summary>
|
||||
|
public Rect Bounds |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_bounds == null) |
||||
|
{ |
||||
|
_bounds = CalculateBounds(); |
||||
|
} |
||||
|
|
||||
|
return _bounds.Value; |
||||
|
} |
||||
|
set => _bounds = value; |
||||
|
} |
||||
|
|
||||
|
public IGlyphRunImpl GlyphRunImpl |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_glyphRunImpl == null) |
||||
|
{ |
||||
|
Initialize(null); |
||||
|
} |
||||
|
|
||||
|
return _glyphRunImpl; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public double GetDistanceFromCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
var distance = 0.0; |
||||
|
|
||||
|
var end = _glyphClusters.AsSpan().BinarySearch((ushort)characterHit.FirstCharacterIndex); |
||||
|
|
||||
|
if (end < 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// If TrailingLength > 0 we have to use the next cluster while TrailingLength != 0
|
||||
|
for (var i = 0; i < end + characterHit.TrailingLength; i++) |
||||
|
{ |
||||
|
if (GlyphAdvances.IsEmpty) |
||||
|
{ |
||||
|
var glyph = GlyphIndices[i]; |
||||
|
|
||||
|
distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
distance += GlyphAdvances[i]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return distance; |
||||
|
} |
||||
|
|
||||
|
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) |
||||
|
{ |
||||
|
// Before
|
||||
|
if (distance < 0) |
||||
|
{ |
||||
|
isInside = false; |
||||
|
|
||||
|
var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _); |
||||
|
|
||||
|
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; |
||||
|
} |
||||
|
|
||||
|
//After
|
||||
|
if (distance > Bounds.Size.Width) |
||||
|
{ |
||||
|
isInside = false; |
||||
|
|
||||
|
var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _); |
||||
|
|
||||
|
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
|
||||
|
//Within
|
||||
|
var currentX = 0.0; |
||||
|
var index = 0; |
||||
|
|
||||
|
for (; index < GlyphIndices.Length; index++) |
||||
|
{ |
||||
|
if (GlyphAdvances.IsEmpty) |
||||
|
{ |
||||
|
var glyph = GlyphIndices[index]; |
||||
|
|
||||
|
currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
currentX += GlyphAdvances[index]; |
||||
|
} |
||||
|
|
||||
|
if (currentX > distance) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (index == GlyphIndices.Length) |
||||
|
{ |
||||
|
index--; |
||||
|
} |
||||
|
|
||||
|
var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); |
||||
|
|
||||
|
isInside = distance < currentX && width > 0; |
||||
|
|
||||
|
var isTrailing = distance > currentX - width / 2; |
||||
|
|
||||
|
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
|
||||
|
public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
|
||||
|
if (characterHit.TrailingLength == 0) |
||||
|
{ |
||||
|
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); |
||||
|
} |
||||
|
|
||||
|
var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); |
||||
|
|
||||
|
return new CharacterHit(nextCharacterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
|
||||
|
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
return characterHit.TrailingLength == 0 ? |
||||
|
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) : |
||||
|
new CharacterHit(characterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
|
||||
|
private class ReverseComparer<T> : IComparer<T> |
||||
|
{ |
||||
|
public int Compare(T x, T y) |
||||
|
{ |
||||
|
return Comparer<T>.Default.Compare(y, x); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default; |
||||
|
private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>(); |
||||
|
|
||||
|
internal CharacterHit FindNearestCharacterHit(int index, out double width) |
||||
|
{ |
||||
|
width = 0.0; |
||||
|
|
||||
|
if (index < 0) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; |
||||
|
|
||||
|
var clusters = _glyphClusters.AsSpan(); |
||||
|
|
||||
|
int start; |
||||
|
|
||||
|
if (index == 0 && clusters[0] == 0) |
||||
|
{ |
||||
|
start = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Find the start of the cluster at the character index.
|
||||
|
start = clusters.BinarySearch((ushort)index, comparer); |
||||
|
} |
||||
|
|
||||
|
// No cluster found.
|
||||
|
if (start < 0) |
||||
|
{ |
||||
|
while (index > 0 && start < 0) |
||||
|
{ |
||||
|
index--; |
||||
|
|
||||
|
start = clusters.BinarySearch((ushort)index, comparer); |
||||
|
} |
||||
|
|
||||
|
if (start < 0) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var trailingLength = 0; |
||||
|
|
||||
|
var currentCluster = clusters[start]; |
||||
|
|
||||
|
while (start > 0 && clusters[start - 1] == currentCluster) |
||||
|
{ |
||||
|
start--; |
||||
|
} |
||||
|
|
||||
|
for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex) |
||||
|
{ |
||||
|
if (_glyphClusters[lastIndex] != currentCluster) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (GlyphAdvances.IsEmpty) |
||||
|
{ |
||||
|
var glyph = GlyphIndices[lastIndex]; |
||||
|
|
||||
|
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
width += GlyphAdvances[lastIndex]; |
||||
|
} |
||||
|
|
||||
|
trailingLength++; |
||||
|
} |
||||
|
|
||||
|
return new CharacterHit(currentCluster, trailingLength); |
||||
|
} |
||||
|
|
||||
|
private Rect CalculateBounds() |
||||
|
{ |
||||
|
var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight; |
||||
|
|
||||
|
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale; |
||||
|
|
||||
|
var width = 0.0; |
||||
|
|
||||
|
if (GlyphAdvances.IsEmpty) |
||||
|
{ |
||||
|
foreach (var glyph in GlyphIndices) |
||||
|
{ |
||||
|
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
foreach (var advance in GlyphAdvances) |
||||
|
{ |
||||
|
width += advance; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new Rect(0, 0, width, height); |
||||
|
} |
||||
|
|
||||
|
private void Set<T>(ref T field, T value) |
||||
|
{ |
||||
|
if (_glyphRunImpl != null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("GlyphRun can't be changed after is has been initialized.'"); |
||||
|
} |
||||
|
|
||||
|
field = value; |
||||
|
} |
||||
|
|
||||
|
private void Initialize(Rect? bounds) |
||||
|
{ |
||||
|
if (GlyphIndices.Length == 0) |
||||
|
{ |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
var glyphCount = GlyphIndices.Length; |
||||
|
|
||||
|
if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount) |
||||
|
{ |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount) |
||||
|
{ |
||||
|
throw new InvalidOperationException(); |
||||
|
} |
||||
|
|
||||
|
_glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width); |
||||
|
|
||||
|
if (bounds.HasValue) |
||||
|
{ |
||||
|
_bounds = bounds; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; |
||||
|
|
||||
|
_bounds = new Rect(0, 0, width, height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void IDisposable.Dispose() |
||||
|
{ |
||||
|
_glyphRunImpl?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public class GlyphRunDrawing : Drawing |
||||
|
{ |
||||
|
public static readonly StyledProperty<IBrush> ForegroundProperty = |
||||
|
AvaloniaProperty.Register<GlyphRunDrawing, IBrush>(nameof(Foreground)); |
||||
|
|
||||
|
public static readonly StyledProperty<GlyphRun> GlyphRunProperty = |
||||
|
AvaloniaProperty.Register<GlyphRunDrawing, GlyphRun>(nameof(GlyphRun)); |
||||
|
|
||||
|
public static readonly StyledProperty<Point> BaselineOriginProperty = |
||||
|
AvaloniaProperty.Register<GlyphRunDrawing, Point>(nameof(BaselineOrigin)); |
||||
|
|
||||
|
public IBrush Foreground |
||||
|
{ |
||||
|
get => GetValue(ForegroundProperty); |
||||
|
set => SetValue(ForegroundProperty, value); |
||||
|
} |
||||
|
|
||||
|
public GlyphRun GlyphRun |
||||
|
{ |
||||
|
get => GetValue(GlyphRunProperty); |
||||
|
set => SetValue(GlyphRunProperty, value); |
||||
|
} |
||||
|
|
||||
|
public Point BaselineOrigin |
||||
|
{ |
||||
|
get => GetValue(BaselineOriginProperty); |
||||
|
set => SetValue(BaselineOriginProperty, value); |
||||
|
} |
||||
|
|
||||
|
public override void Draw(DrawingContext context) |
||||
|
{ |
||||
|
if (GlyphRun == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); |
||||
|
} |
||||
|
|
||||
|
public override Rect GetBounds() |
||||
|
{ |
||||
|
return GlyphRun?.Bounds ?? default; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Platform |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Actual implementation of a glyph run that stores platform dependent resources.
|
||||
|
/// </summary>
|
||||
|
public interface IGlyphRunImpl : IDisposable { } |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Rendering.SceneGraph |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A node in the scene graph which represents a text draw.
|
||||
|
/// </summary>
|
||||
|
internal class GlyphRunNode : BrushDrawOperation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="GlyphRunNode"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="transform">The transform.</param>
|
||||
|
/// <param name="foreground">The foreground brush.</param>
|
||||
|
/// <param name="glyphRun">The glyph run to draw.</param>
|
||||
|
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
|
||||
|
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
|
||||
|
public GlyphRunNode( |
||||
|
Matrix transform, |
||||
|
IBrush foreground, |
||||
|
GlyphRun glyphRun, |
||||
|
Point baselineOrigin, |
||||
|
IDictionary<IVisual, Scene> childScenes = null) |
||||
|
: base(glyphRun.Bounds, transform, null) |
||||
|
{ |
||||
|
Transform = transform; |
||||
|
Foreground = foreground?.ToImmutable(); |
||||
|
GlyphRun = glyphRun; |
||||
|
BaselineOrigin = baselineOrigin; |
||||
|
ChildScenes = childScenes; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the transform with which the node will be drawn.
|
||||
|
/// </summary>
|
||||
|
public Matrix Transform { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the foreground brush.
|
||||
|
/// </summary>
|
||||
|
public IBrush Foreground { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text to draw.
|
||||
|
/// </summary>
|
||||
|
public GlyphRun GlyphRun { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the baseline origin.
|
||||
|
/// </summary>
|
||||
|
public Point BaselineOrigin { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override IDictionary<IVisual, Scene> ChildScenes { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Render(IDrawingContextImpl context) |
||||
|
{ |
||||
|
context.Transform = Transform; |
||||
|
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines if this draw operation equals another.
|
||||
|
/// </summary>
|
||||
|
/// <param name="transform">The transform of the other draw operation.</param>
|
||||
|
/// <param name="foreground">The foreground of the other draw operation.</param>
|
||||
|
/// <param name="glyphRun">The text of the other draw operation.</param>
|
||||
|
/// <returns>True if the draw operations are the same, otherwise false.</returns>
|
||||
|
/// <remarks>
|
||||
|
/// The properties of the other draw operation are passed in as arguments to prevent
|
||||
|
/// allocation of a not-yet-constructed draw operation object.
|
||||
|
/// </remarks>
|
||||
|
internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) |
||||
|
{ |
||||
|
return transform == Transform && |
||||
|
Equals(foreground, Foreground) && |
||||
|
Equals(glyphRun, GlyphRun); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool HitTest(Point p) => Bounds.Contains(p); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,154 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Utility |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of elements in the slice.</typeparam>
|
||||
|
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> |
||||
|
{ |
||||
|
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { } |
||||
|
|
||||
|
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length) |
||||
|
{ |
||||
|
Buffer = buffer; |
||||
|
Start = start; |
||||
|
Length = length; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the start.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The start.
|
||||
|
/// </value>
|
||||
|
public int Start { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the end.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The end.
|
||||
|
/// </value>
|
||||
|
public int End => Start + Length - 1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the length.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The length.
|
||||
|
/// </value>
|
||||
|
public int Length { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySpan{T}"/> is Empty.
|
||||
|
/// </summary>
|
||||
|
public bool IsEmpty => Length == 0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The buffer.
|
||||
|
/// </summary>
|
||||
|
public ReadOnlyMemory<T> Buffer { get; } |
||||
|
|
||||
|
public T this[int index] => Buffer.Span[Start + index]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a span of the underlying buffer.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="ReadOnlySpan{T}"/> of the underlying buffer.</returns>
|
||||
|
public ReadOnlySpan<T> AsSpan() |
||||
|
{ |
||||
|
return Buffer.Span.Slice(Start, Length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The start of the sub slice.</param>
|
||||
|
/// <param name="length">The length of the sub slice.</param>
|
||||
|
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
|
||||
|
public ReadOnlySlice<T> AsSlice(int start, int length) |
||||
|
{ |
||||
|
if (start < 0 || start >= Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(start)); |
||||
|
} |
||||
|
|
||||
|
if (Start + start > End) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new ReadOnlySlice<T>(Buffer, Start + start, length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a specified number of contiguous elements from the start of the slice.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of elements to return.</param>
|
||||
|
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
|
||||
|
public ReadOnlySlice<T> Take(int length) |
||||
|
{ |
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new ReadOnlySlice<T>(Buffer, Start, length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
|
||||
|
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
|
||||
|
public ReadOnlySlice<T> Skip(int length) |
||||
|
{ |
||||
|
if (length > Length) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(length)); |
||||
|
} |
||||
|
|
||||
|
return new ReadOnlySlice<T>(Buffer, Start + length, Length - length); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns an enumerator for the slice.
|
||||
|
/// </summary>
|
||||
|
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator() |
||||
|
{ |
||||
|
return new ImmutableReadOnlyListStructEnumerator<T>(this); |
||||
|
} |
||||
|
|
||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() |
||||
|
{ |
||||
|
return GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
IEnumerator IEnumerable.GetEnumerator() |
||||
|
{ |
||||
|
return GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
int IReadOnlyCollection<T>.Count => Length; |
||||
|
|
||||
|
T IReadOnlyList<T>.this[int index] => this[index]; |
||||
|
|
||||
|
public static implicit operator ReadOnlySlice<T>(T[] array) |
||||
|
{ |
||||
|
return new ReadOnlySlice<T>(array); |
||||
|
} |
||||
|
|
||||
|
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory) |
||||
|
{ |
||||
|
return new ReadOnlySlice<T>(memory); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using Avalonia.Platform; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Skia |
||||
|
{ |
||||
|
/// <inheritdoc />
|
||||
|
public class GlyphRunImpl : IGlyphRunImpl |
||||
|
{ |
||||
|
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) |
||||
|
{ |
||||
|
Paint = paint; |
||||
|
TextBlob = textBlob; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the paint to draw with.
|
||||
|
/// </summary>
|
||||
|
public SKPaint Paint { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text blob to draw.
|
||||
|
/// </summary>
|
||||
|
public SKTextBlob TextBlob { get; } |
||||
|
|
||||
|
void IDisposable.Dispose() |
||||
|
{ |
||||
|
TextBlob.Dispose(); |
||||
|
Paint.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Direct2D1.Media |
||||
|
{ |
||||
|
internal class GlyphRunImpl : IGlyphRunImpl |
||||
|
{ |
||||
|
public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) |
||||
|
{ |
||||
|
GlyphRun = glyphRun; |
||||
|
} |
||||
|
|
||||
|
public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
GlyphRun?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.UnitTests |
||||
|
{ |
||||
|
public class MockGlyphTypeface : IGlyphTypefaceImpl |
||||
|
{ |
||||
|
public short DesignEmHeight => 10; |
||||
|
public int Ascent => 100; |
||||
|
public int Descent => 0; |
||||
|
public int LineGap { get; } |
||||
|
public int UnderlinePosition { get; } |
||||
|
public int UnderlineThickness { get; } |
||||
|
public int StrikethroughPosition { get; } |
||||
|
public int StrikethroughThickness { get; } |
||||
|
public bool IsFixedPitch { get; } |
||||
|
|
||||
|
public ushort GetGlyph(uint codepoint) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
||||
|
{ |
||||
|
return new ushort[codepoints.Length]; |
||||
|
} |
||||
|
|
||||
|
public int GetGlyphAdvance(ushort glyph) |
||||
|
{ |
||||
|
return 100; |
||||
|
} |
||||
|
|
||||
|
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
||||
|
{ |
||||
|
var advances = new int[glyphs.Length]; |
||||
|
|
||||
|
for (var i = 0; i < advances.Length; i++) |
||||
|
{ |
||||
|
advances[i] = 100; |
||||
|
} |
||||
|
|
||||
|
return advances; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() { } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Visuals.UnitTests.Media |
||||
|
{ |
||||
|
public class GlyphRunTests : TestWithServicesBase |
||||
|
{ |
||||
|
public GlyphRunTests() |
||||
|
{ |
||||
|
AvaloniaLocator.CurrentMutable |
||||
|
.Bind<IPlatformRenderInterface>().ToSingleton<MockPlatformRenderInterface>(); |
||||
|
} |
||||
|
|
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 2, 0, true)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)] |
||||
|
[Theory] |
||||
|
public void Should_Get_TextBounds_FromDistance(double[] advances, ushort[] clusters, double distance, int start, |
||||
|
int trailingLengthExpected, bool isInsideExpected) |
||||
|
{ |
||||
|
using (var glyphRun = CreateGlyphRun(advances, clusters)) |
||||
|
{ |
||||
|
var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); |
||||
|
|
||||
|
Assert.Equal(start, textBounds.FirstCharacterIndex); |
||||
|
|
||||
|
Assert.Equal(trailingLengthExpected, textBounds.TrailingLength); |
||||
|
|
||||
|
Assert.Equal(isInsideExpected, isInside); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)] |
||||
|
[Theory] |
||||
|
public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel, |
||||
|
int index, int expectedIndex, int expectedLength, double expectedWidth) |
||||
|
{ |
||||
|
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) |
||||
|
{ |
||||
|
var textBounds = glyphRun.FindNearestCharacterHit(index, out var width); |
||||
|
|
||||
|
Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex); |
||||
|
|
||||
|
Assert.Equal(expectedLength, textBounds.TrailingLength); |
||||
|
|
||||
|
Assert.Equal(expectedWidth, width, 2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)] |
||||
|
[Theory] |
||||
|
public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters, |
||||
|
int currentIndex, int currentLength, |
||||
|
int nextIndex, int nextLength, |
||||
|
int bidiLevel) |
||||
|
{ |
||||
|
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) |
||||
|
{ |
||||
|
var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); |
||||
|
|
||||
|
Assert.Equal(nextIndex, characterHit.FirstCharacterIndex); |
||||
|
|
||||
|
Assert.Equal(nextLength, characterHit.TrailingLength); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)] |
||||
|
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)] |
||||
|
[Theory] |
||||
|
public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters, |
||||
|
int currentIndex, int currentLength, |
||||
|
int previousIndex, int previousLength, |
||||
|
int bidiLevel) |
||||
|
{ |
||||
|
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) |
||||
|
{ |
||||
|
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); |
||||
|
|
||||
|
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex); |
||||
|
|
||||
|
Assert.Equal(previousLength, characterHit.TrailingLength); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0) |
||||
|
{ |
||||
|
var count = glyphAdvances.Length; |
||||
|
var glyphIndices = new ushort[count]; |
||||
|
|
||||
|
var bounds = new Rect(0, 0, count * 10, 10); |
||||
|
|
||||
|
return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances, |
||||
|
glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue