Browse Source

Add GlyphRun support

pull/3202/head
Benedikt Schroeder 6 years ago
parent
commit
ab5e062deb
  1. 4
      build/HarfBuzzSharp.props
  2. 4
      build/SkiaSharp.props
  3. 3
      samples/RenderDemo/MainWindow.xaml
  4. 14
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  5. 80
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  6. 68
      src/Avalonia.Visuals/Media/CharacterHit.cs
  7. 16
      src/Avalonia.Visuals/Media/DrawingContext.cs
  8. 459
      src/Avalonia.Visuals/Media/GlyphRun.cs
  9. 50
      src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
  10. 5
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  11. 8
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  12. 12
      src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
  13. 5
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  14. 8
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  15. 15
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  16. 91
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  17. 154
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  18. 14
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 35
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  20. 5
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  21. 85
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  22. 50
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  23. 16
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  24. 19
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  25. 9
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  26. 47
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  27. 6
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  28. 112
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
  29. 2
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

4
build/HarfBuzzSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" 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" />
</ItemGroup>
</Project>

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" 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" />
</ItemGroup>
</Project>

3
samples/RenderDemo/MainWindow.xaml

@ -41,6 +41,9 @@
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

14
samples/RenderDemo/Pages/GlyphRunPage.xaml

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

80
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -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;
}
}
}

68
src/Avalonia.Visuals/Media/CharacterHit.cs

@ -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);
}
}
}

16
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -187,6 +187,22 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
Contract.Requires<ArgumentNullException>(glyphRun != null);
if (foreground != null)
{
PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin);
}
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>

459
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -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();
}
}
}

50
src/Avalonia.Visuals/Media/GlyphRunDrawing.cs

@ -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;
}
}
}

5
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -66,6 +66,11 @@ namespace Avalonia.Media
/// </summary>
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
public bool IsFixedPitch => PlatformImpl.IsFixedPitch;
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

8
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -86,6 +86,14 @@ namespace Avalonia.Platform
/// <param name="text">The text.</param>
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin);
/// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer
/// for the current render target.

12
src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs

@ -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 { }
}

5
src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs

@ -47,6 +47,11 @@ namespace Avalonia.Platform
/// </summary>
int StrikethroughThickness { get; }
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
bool IsFixedPitch { get; }
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

8
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -117,5 +117,13 @@ namespace Avalonia.Platform
/// </summary>
/// <returns>The font manager.</returns>
IFontManagerImpl CreateFontManager();
/// <summary>
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
}
}

15
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -190,6 +190,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
/// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
{
Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");

91
src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs

@ -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);
}
}

154
src/Avalonia.Visuals/Utility/ReadOnlySlice.cs

@ -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);
}
}
}

14
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -232,6 +232,20 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint);
}
}
/// <inheritdoc />
public IRenderTargetBitmapImpl CreateLayer(Size size)
{

35
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -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();
}
}
}

5
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -61,6 +61,8 @@ namespace Avalonia.Skia
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = Typeface.IsFixedPitch;
}
public Face Face { get; }
@ -93,6 +95,9 @@ namespace Avalonia.Skia
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

85
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -156,5 +156,90 @@ namespace Avalonia.Skia
{
return new FontManagerImpl();
}
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var count = glyphRun.GlyphIndices.Length;
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var typeface = glyphTypeface.Typeface;
var paint = new SKPaint
{
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
{
width = 0;
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
if (!glyphTypeface.IsFixedPitch)
{
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
}
else
{
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
width = currentX;
}
var textBlob = textBlobBuilder.Build();
return new GlyphRunImpl(paint, textBlob);
}
}
}
}

50
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
namespace Avalonia
{
@ -196,5 +199,52 @@ namespace Avalonia.Direct2D1
{
return new FontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var glyphCount = glyphRun.GlyphIndices.Length;
var run = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypeface.FontFace,
FontSize = (float)glyphRun.FontRenderingEmSize
};
var indices = new short[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphRun.GlyphIndices[i];
}
run.Indices = indices;
run.Advances = new float[glyphCount];
width = 0;
for (var i = 0; i < glyphCount; i++)
{
run.Advances[i] = (float)glyphRun.GlyphAdvances[i];
width += run.Advances[i];
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var offset = glyphRun.GlyphOffsets[i];
run.Offsets[i] = new GlyphOffset
{
AdvanceOffset = (float)offset.X,
AscenderOffset = (float)offset.Y
};
}
return new GlyphRunImpl(run);
}
}
}

16
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -316,6 +316,22 @@ namespace Avalonia.Direct2D1.Media
}
}
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin"></param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
_renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural);
}
}
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
if (_layerFactory != null)

19
src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs

@ -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();
}
}
}

9
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
FontFace = new FontFace(DWFont);
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
Face = new Face(GetTable);
@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = FontFace.IsMonospacedFont;
}
private Blob GetTable(Face face, Tag tag)
@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace FontFace { get; }
public FontFace1 FontFace { get; }
public Face Face { get; }
@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

47
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -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() { }
}
}

6
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -83,5 +83,11 @@ namespace Avalonia.UnitTests
{
return new MockFontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
width = 0;
return Mock.Of<IGlyphRunImpl>();
}
}
}

112
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@ -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);
}
}
}

2
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -51,7 +51,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}

Loading…
Cancel
Save