29 changed files with 1389 additions and 7 deletions
@ -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> |
|||
|
|||
@ -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> |
|||
|
|||
@ -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