105 changed files with 11816 additions and 3849 deletions
@ -0,0 +1,7 @@ |
|||
<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.FormattedTextPage"> |
|||
</UserControl> |
|||
@ -0,0 +1,60 @@ |
|||
using System.Globalization; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Media; |
|||
|
|||
namespace RenderDemo.Pages |
|||
{ |
|||
public class FormattedTextPage : UserControl |
|||
{ |
|||
public FormattedTextPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor"; |
|||
|
|||
// Create the initial formatted text string.
|
|||
var formattedText = new FormattedText( |
|||
testString, |
|||
CultureInfo.GetCultureInfo("en-us"), |
|||
FlowDirection.LeftToRight, |
|||
new Typeface("Verdana"), |
|||
32, |
|||
Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 }; |
|||
|
|||
// Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
|
|||
|
|||
// Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
|
|||
// The font size is calculated in terms of points -- not as device-independent pixels.
|
|||
formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5); |
|||
|
|||
// Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
|
|||
formattedText.SetFontWeight(FontWeight.Bold, 6, 11); |
|||
|
|||
var gradient = new LinearGradientBrush |
|||
{ |
|||
GradientStops = |
|||
new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) }, |
|||
StartPoint = new RelativePoint(0,0, RelativeUnit.Relative), |
|||
EndPoint = new RelativePoint(0,1, RelativeUnit.Relative) |
|||
}; |
|||
|
|||
// Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
|
|||
formattedText.SetForegroundBrush(gradient, 6, 11); |
|||
|
|||
// Use an Italic font style beginning at the 28th character and continuing for 28 characters.
|
|||
formattedText.SetFontStyle(FontStyle.Italic, 28, 28); |
|||
|
|||
context.DrawText(formattedText, new Point(10, 0)); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
@ -1,29 +0,0 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Stores information about a line of <see cref="FormattedText"/>.
|
|||
/// </summary>
|
|||
public class FormattedTextLine |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FormattedTextLine"/> class.
|
|||
/// </summary>
|
|||
/// <param name="length">The length of the line, in characters.</param>
|
|||
/// <param name="height">The height of the line, in pixels.</param>
|
|||
public FormattedTextLine(int length, double height) |
|||
{ |
|||
Length = length; |
|||
Height = height; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length of the line, in characters.
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the height of the line, in pixels.
|
|||
/// </summary>
|
|||
public double Height { get; } |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Describes the formatting for a span of text in a <see cref="FormattedText"/> object.
|
|||
/// </summary>
|
|||
public class FormattedTextStyleSpan |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FormattedTextStyleSpan"/> class.
|
|||
/// </summary>
|
|||
/// <param name="startIndex">The index of the first character in the span.</param>
|
|||
/// <param name="length">The length of the span.</param>
|
|||
/// <param name="foregroundBrush">The span's foreground brush.</param>
|
|||
public FormattedTextStyleSpan( |
|||
int startIndex, |
|||
int length, |
|||
IBrush? foregroundBrush = null) |
|||
{ |
|||
StartIndex = startIndex; |
|||
Length = length; |
|||
ForegroundBrush = foregroundBrush; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the first character in the span.
|
|||
/// </summary>
|
|||
public int StartIndex { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the length of the span.
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the span's foreground brush.
|
|||
/// </summary>
|
|||
public IBrush? ForegroundBrush { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,293 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
public sealed class ShapedBuffer : IList<GlyphInfo> |
|||
{ |
|||
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters(); |
|||
|
|||
public ShapedBuffer(ReadOnlySlice<char> text, int length, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) |
|||
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel) |
|||
{ |
|||
|
|||
} |
|||
|
|||
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) |
|||
{ |
|||
Text = text; |
|||
GlyphInfos = glyphInfos; |
|||
GlyphTypeface = glyphTypeface; |
|||
FontRenderingEmSize = fontRenderingEmSize; |
|||
BidiLevel = bidiLevel; |
|||
} |
|||
|
|||
internal ArraySlice<GlyphInfo> GlyphInfos { get; } |
|||
|
|||
public ReadOnlySlice<char> Text { get; } |
|||
|
|||
public int Length => GlyphInfos.Length; |
|||
|
|||
public GlyphTypeface GlyphTypeface { get; } |
|||
|
|||
public double FontRenderingEmSize { get; } |
|||
|
|||
public sbyte BidiLevel { get; } |
|||
|
|||
public bool IsLeftToRight => (BidiLevel & 1) == 0; |
|||
|
|||
public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos); |
|||
|
|||
public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos); |
|||
|
|||
public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos); |
|||
|
|||
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos); |
|||
|
|||
/// <summary>
|
|||
/// Finds a glyph index for given character index.
|
|||
/// </summary>
|
|||
/// <param name="characterIndex">The character index.</param>
|
|||
/// <returns>
|
|||
/// The glyph index.
|
|||
/// </returns>
|
|||
private int FindGlyphIndex(int characterIndex) |
|||
{ |
|||
if (characterIndex < GlyphInfos[0].GlyphCluster) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster) |
|||
{ |
|||
return GlyphInfos.Length - 1; |
|||
} |
|||
|
|||
|
|||
var comparer = s_clusterComparer; |
|||
|
|||
var clusters = GlyphInfos.Span; |
|||
|
|||
var searchValue = new GlyphInfo(0, characterIndex); |
|||
|
|||
var start = clusters.BinarySearch(searchValue, comparer); |
|||
|
|||
if (start < 0) |
|||
{ |
|||
while (characterIndex > 0 && start < 0) |
|||
{ |
|||
characterIndex--; |
|||
|
|||
searchValue = new GlyphInfo(0, characterIndex); |
|||
|
|||
start = clusters.BinarySearch(searchValue, comparer); |
|||
} |
|||
|
|||
if (start < 0) |
|||
{ |
|||
return -1; |
|||
} |
|||
} |
|||
|
|||
while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) |
|||
{ |
|||
start--; |
|||
} |
|||
|
|||
return start; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Splits the <see cref="TextRun"/> at specified length.
|
|||
/// </summary>
|
|||
/// <param name="length">The length.</param>
|
|||
/// <returns>The split result.</returns>
|
|||
internal SplitResult<ShapedBuffer> Split(int length) |
|||
{ |
|||
var glyphCount = FindGlyphIndex(Text.Start + length); |
|||
|
|||
if (Text.Length == length) |
|||
{ |
|||
return new SplitResult<ShapedBuffer>(this, null); |
|||
} |
|||
|
|||
if (Text.Length == glyphCount) |
|||
{ |
|||
return new SplitResult<ShapedBuffer>(this, null); |
|||
} |
|||
|
|||
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|||
|
|||
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|||
|
|||
return new SplitResult<ShapedBuffer>(first, second); |
|||
} |
|||
|
|||
int ICollection<GlyphInfo>.Count => throw new NotImplementedException(); |
|||
|
|||
bool ICollection<GlyphInfo>.IsReadOnly => true; |
|||
|
|||
public GlyphInfo this[int index] |
|||
{ |
|||
get => GlyphInfos[index]; |
|||
set => GlyphInfos[index] = value; |
|||
} |
|||
|
|||
int IList<GlyphInfo>.IndexOf(GlyphInfo item) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
void IList<GlyphInfo>.Insert(int index, GlyphInfo item) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
void IList<GlyphInfo>.RemoveAt(int index) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
void ICollection<GlyphInfo>.Add(GlyphInfo item) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
void ICollection<GlyphInfo>.Clear() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
bool ICollection<GlyphInfo>.Contains(GlyphInfo item) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
bool ICollection<GlyphInfo>.Remove(GlyphInfo item) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator(); |
|||
|
|||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
private class CompareClusters : IComparer<GlyphInfo> |
|||
{ |
|||
private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default; |
|||
|
|||
public int Compare(GlyphInfo x, GlyphInfo y) |
|||
{ |
|||
return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); |
|||
} |
|||
} |
|||
|
|||
private readonly struct GlyphAdvanceList : IReadOnlyList<double> |
|||
{ |
|||
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
|||
|
|||
public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos) |
|||
{ |
|||
_glyphInfos = glyphInfos; |
|||
} |
|||
|
|||
public double this[int index] => _glyphInfos[index].GlyphAdvance; |
|||
|
|||
public int Count => _glyphInfos.Length; |
|||
|
|||
public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this); |
|||
|
|||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
|
|||
private readonly struct GlyphIndexList : IReadOnlyList<ushort> |
|||
{ |
|||
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
|||
|
|||
public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos) |
|||
{ |
|||
_glyphInfos = glyphInfos; |
|||
} |
|||
|
|||
public ushort this[int index] => _glyphInfos[index].GlyphIndex; |
|||
|
|||
public int Count => _glyphInfos.Length; |
|||
|
|||
public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this); |
|||
|
|||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
|
|||
private readonly struct GlyphClusterList : IReadOnlyList<int> |
|||
{ |
|||
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
|||
|
|||
public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos) |
|||
{ |
|||
_glyphInfos = glyphInfos; |
|||
} |
|||
|
|||
public int this[int index] => _glyphInfos[index].GlyphCluster; |
|||
|
|||
public int Count => _glyphInfos.Length; |
|||
|
|||
public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this); |
|||
|
|||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
|
|||
private readonly struct GlyphOffsetList : IReadOnlyList<Vector> |
|||
{ |
|||
private readonly ArraySlice<GlyphInfo> _glyphInfos; |
|||
|
|||
public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos) |
|||
{ |
|||
_glyphInfos = glyphInfos; |
|||
} |
|||
|
|||
public Vector this[int index] => _glyphInfos[index].GlyphOffset; |
|||
|
|||
public int Count => _glyphInfos.Length; |
|||
|
|||
public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this); |
|||
|
|||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
|
|||
public readonly struct GlyphInfo |
|||
{ |
|||
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) |
|||
{ |
|||
GlyphIndex = glyphIndex; |
|||
GlyphAdvance = glyphAdvance; |
|||
GlyphCluster = glyphCluster; |
|||
GlyphOffset = glyphOffset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get the glyph index.
|
|||
/// </summary>
|
|||
public ushort GlyphIndex { get; } |
|||
|
|||
/// <summary>
|
|||
/// Get the glyph cluster.
|
|||
/// </summary>
|
|||
public int GlyphCluster { get; } |
|||
|
|||
/// <summary>
|
|||
/// Get the glyph advance.
|
|||
/// </summary>
|
|||
public double GlyphAdvance { get; } |
|||
|
|||
/// <summary>
|
|||
/// Get the glyph offset.
|
|||
/// </summary>
|
|||
public Vector GlyphOffset { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
internal readonly struct SplitResult<T> |
|||
{ |
|||
public SplitResult(T first, T? second) |
|||
{ |
|||
First = first; |
|||
|
|||
Second = second; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the first part.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The first part.
|
|||
/// </value>
|
|||
public T First { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the second part.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The second part.
|
|||
/// </value>
|
|||
public T? Second { get; } |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,182 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// Ported from: https://github.com/SixLabors/Fonts/
|
|||
|
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.TextFormatting.Unicode |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a unicode string and all associated attributes
|
|||
/// for each character required for the bidirectional Unicode algorithm
|
|||
/// </summary>
|
|||
internal class BidiData |
|||
{ |
|||
private ArrayBuilder<BidiClass> _classes; |
|||
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes; |
|||
private ArrayBuilder<int> _pairedBracketValues; |
|||
private ArrayBuilder<BidiClass> _savedClasses; |
|||
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes; |
|||
private ArrayBuilder<sbyte> _tempLevelBuffer; |
|||
|
|||
public BidiData(sbyte paragraphEmbeddingLevel = 0) |
|||
{ |
|||
ParagraphEmbeddingLevel = paragraphEmbeddingLevel; |
|||
} |
|||
|
|||
public BidiData(ReadOnlySlice<char> text, sbyte paragraphEmbeddingLevel = 0) : this(paragraphEmbeddingLevel) |
|||
{ |
|||
Append(text); |
|||
} |
|||
|
|||
public sbyte ParagraphEmbeddingLevel { get; private set; } |
|||
|
|||
public bool HasBrackets { get; private set; } |
|||
|
|||
public bool HasEmbeddings { get; private set; } |
|||
|
|||
public bool HasIsolates { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the length of the data held by the BidiData
|
|||
/// </summary>
|
|||
public int Length{get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the bidi character type of each code point
|
|||
/// </summary>
|
|||
public ArraySlice<BidiClass> Classes { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the paired bracket type for each code point
|
|||
/// </summary>
|
|||
public ArraySlice<BidiPairedBracketType> PairedBracketTypes { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the paired bracket value for code point
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The paired bracket values are the code points
|
|||
/// of each character where the opening code point
|
|||
/// is replaced with the closing code point for easier
|
|||
/// matching. Also, bracket code points are mapped
|
|||
/// to their canonical equivalents
|
|||
/// </remarks>
|
|||
public ArraySlice<int> PairedBracketValues { get; private set; } |
|||
|
|||
public void Append(ReadOnlySlice<char> text) |
|||
{ |
|||
_classes.Add(text.Length); |
|||
_pairedBracketTypes.Add(text.Length); |
|||
_pairedBracketValues.Add(text.Length); |
|||
|
|||
var i = Length; |
|||
|
|||
var codePointEnumerator = new CodepointEnumerator(text); |
|||
|
|||
while (codePointEnumerator.MoveNext()) |
|||
{ |
|||
var codepoint = codePointEnumerator.Current; |
|||
|
|||
// Look up BiDiClass
|
|||
var dir = codepoint.BiDiClass; |
|||
|
|||
_classes[i] = dir; |
|||
|
|||
switch (dir) |
|||
{ |
|||
case BidiClass.LeftToRightEmbedding: |
|||
case BidiClass.LeftToRightOverride: |
|||
case BidiClass.RightToLeftEmbedding: |
|||
case BidiClass.RightToLeftOverride: |
|||
case BidiClass.PopDirectionalFormat: |
|||
{ |
|||
HasEmbeddings = true; |
|||
break; |
|||
} |
|||
|
|||
case BidiClass.LeftToRightIsolate: |
|||
case BidiClass.RightToLeftIsolate: |
|||
case BidiClass.FirstStrongIsolate: |
|||
case BidiClass.PopDirectionalIsolate: |
|||
{ |
|||
HasIsolates = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Lookup paired bracket types
|
|||
var pbt = codepoint.PairedBracketType; |
|||
|
|||
_pairedBracketTypes[i] = pbt; |
|||
|
|||
if (pbt == BidiPairedBracketType.Open) |
|||
{ |
|||
// Opening bracket types can never have a null pairing.
|
|||
codepoint.TryGetPairedBracket(out var paired); |
|||
|
|||
_pairedBracketValues[i] = Codepoint.GetCanonicalType(paired).Value; |
|||
|
|||
HasBrackets = true; |
|||
} |
|||
else if (pbt == BidiPairedBracketType.Close) |
|||
{ |
|||
_pairedBracketValues[i] = Codepoint.GetCanonicalType(codepoint).Value; |
|||
|
|||
HasBrackets = true; |
|||
} |
|||
|
|||
i++; |
|||
} |
|||
|
|||
Length = i; |
|||
|
|||
Classes = _classes.AsSlice(0, Length); |
|||
PairedBracketTypes = _pairedBracketTypes.AsSlice(0, Length); |
|||
PairedBracketValues = _pairedBracketValues.AsSlice(0, Length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Save the Types and PairedBracketTypes of this BiDiData
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is used when processing embedded style runs with
|
|||
/// BiDiClass overrides. Text layout process saves the data,
|
|||
/// overrides the style runs to neutral, processes the bidi
|
|||
/// data for the entire paragraph and then restores this data
|
|||
/// before processing the embedded runs.
|
|||
/// </remarks>
|
|||
public void SaveTypes() |
|||
{ |
|||
// Capture the types data
|
|||
_savedClasses.Clear(); |
|||
_savedClasses.Add(_classes.AsSlice()); |
|||
_savedPairedBracketTypes.Clear(); |
|||
_savedPairedBracketTypes.Add(_pairedBracketTypes.AsSlice()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Restore the data saved by SaveTypes
|
|||
/// </summary>
|
|||
public void RestoreTypes() |
|||
{ |
|||
_classes.Clear(); |
|||
_classes.Add(_savedClasses.AsSlice()); |
|||
_pairedBracketTypes.Clear(); |
|||
_pairedBracketTypes.Add(_savedPairedBracketTypes.AsSlice()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a temporary level buffer. Used by the text layout process when
|
|||
/// resolving style runs with different BiDiClass.
|
|||
/// </summary>
|
|||
/// <param name="length">Length of the required ExpandableBuffer</param>
|
|||
/// <returns>An uninitialized level ExpandableBuffer</returns>
|
|||
public ArraySlice<sbyte> GetTempLevelBuffer(int length) |
|||
{ |
|||
_tempLevelBuffer.Clear(); |
|||
|
|||
return _tempLevelBuffer.Add(length, false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Avalonia.Media.TextFormatting.Unicode |
|||
{ |
|||
public enum BidiPairedBracketType |
|||
{ |
|||
None, //n
|
|||
Close, //c
|
|||
Open, //o
|
|||
} |
|||
} |
|||
@ -1,23 +1,38 @@ |
|||
using Avalonia.Media.TextFormatting; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Holds a hit test result from a <see cref="FormattedText"/>.
|
|||
/// Holds a hit test result from a <see cref="TextLayout"/>.
|
|||
/// </summary>
|
|||
public class TextHitTestResult |
|||
public readonly struct TextHitTestResult |
|||
{ |
|||
public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing) |
|||
{ |
|||
CharacterHit = characterHit; |
|||
TextPosition = textPosition; |
|||
IsInside = isInside; |
|||
IsTrailing = isTrailing; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the character hit of the hit test result.
|
|||
/// </summary>
|
|||
public CharacterHit CharacterHit { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the point is inside the bounds of the text.
|
|||
/// Gets a value indicating whether the point is inside the bounds of the text.
|
|||
/// </summary>
|
|||
public bool IsInside { get; set; } |
|||
public bool IsInside { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the hit character in the text.
|
|||
/// </summary>
|
|||
public int TextPosition { get; set; } |
|||
public int TextPosition { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the hit is on the trailing edge of the character.
|
|||
/// </summary>
|
|||
public bool IsTrailing { get; set; } |
|||
public bool IsTrailing { get; } |
|||
} |
|||
} |
|||
|
|||
@ -1,59 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the platform-specific interface for <see cref="FormattedText"/>.
|
|||
/// </summary>
|
|||
public interface IFormattedTextImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the constraint of the text.
|
|||
/// </summary>
|
|||
Size Constraint { get; } |
|||
|
|||
/// <summary>
|
|||
/// The measured bounds of the text.
|
|||
/// </summary>
|
|||
Rect Bounds{ get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the text.
|
|||
/// </summary>
|
|||
string Text { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the lines in the text.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// A collection of <see cref="FormattedTextLine"/> objects.
|
|||
/// </returns>
|
|||
IEnumerable<FormattedTextLine> GetLines(); |
|||
|
|||
/// <summary>
|
|||
/// Hit tests a point in the text.
|
|||
/// </summary>
|
|||
/// <param name="point">The point.</param>
|
|||
/// <returns>
|
|||
/// A <see cref="TextHitTestResult"/> describing the result of the hit test.
|
|||
/// </returns>
|
|||
TextHitTestResult HitTestPoint(Point point); |
|||
|
|||
/// <summary>
|
|||
/// Gets the bounds rectangle that the specified character occupies.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the character.</param>
|
|||
/// <returns>The character bounds.</returns>
|
|||
Rect HitTestTextPosition(int index); |
|||
|
|||
/// <summary>
|
|||
/// Gets the bounds rectangles that the specified text range occupies.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the first character.</param>
|
|||
/// <param name="length">The number of characters in the text range.</param>
|
|||
/// <returns>The character bounds.</returns>
|
|||
IEnumerable<Rect> HitTestTextRange(int index, int length); |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
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 TextNode : BrushDrawOperation |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TextNode"/> class.
|
|||
/// </summary>
|
|||
/// <param name="transform">The transform.</param>
|
|||
/// <param name="foreground">The foreground brush.</param>
|
|||
/// <param name="origin">The draw origin.</param>
|
|||
/// <param name="text">The text to draw.</param>
|
|||
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
|
|||
public TextNode( |
|||
Matrix transform, |
|||
IBrush foreground, |
|||
Point origin, |
|||
IFormattedTextImpl text, |
|||
IDictionary<IVisual, Scene>? childScenes = null) |
|||
: base(text.Bounds.Translate(origin), transform) |
|||
{ |
|||
Transform = transform; |
|||
Foreground = foreground.ToImmutable(); |
|||
Origin = origin; |
|||
Text = text; |
|||
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 draw origin.
|
|||
/// </summary>
|
|||
public Point Origin { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the text to draw.
|
|||
/// </summary>
|
|||
public IFormattedTextImpl Text { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override IDictionary<IVisual, Scene>? ChildScenes { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Render(IDrawingContextImpl context) |
|||
{ |
|||
context.Transform = Transform; |
|||
context.DrawText(Foreground, Origin, Text); |
|||
} |
|||
|
|||
/// <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="origin">The draw origin of the other draw operation.</param>
|
|||
/// <param name="text">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, Point origin, IFormattedTextImpl text) |
|||
{ |
|||
return transform == Transform && |
|||
Equals(foreground, Foreground) && |
|||
origin == Origin && |
|||
Equals(text, Text); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool HitTest(Point p) => Bounds.Contains(p); |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// Ported from: https://github.com/SixLabors/Fonts/
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// A helper type for avoiding allocations while building arrays.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of item contained in the array.</typeparam>
|
|||
internal struct ArrayBuilder<T> |
|||
where T : struct |
|||
{ |
|||
private const int DefaultCapacity = 4; |
|||
private const int MaxCoreClrArrayLength = 0x7FeFFFFF; |
|||
|
|||
// Starts out null, initialized on first Add.
|
|||
private T[] _data; |
|||
private int _size; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of items in the array.
|
|||
/// </summary>
|
|||
public int Length |
|||
{ |
|||
get => _size; |
|||
|
|||
set |
|||
{ |
|||
if (value == _size) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (value > 0) |
|||
{ |
|||
EnsureCapacity(value); |
|||
|
|||
_size = value; |
|||
} |
|||
else |
|||
{ |
|||
_size = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a reference to specified element of the array.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the element to return.</param>
|
|||
/// <returns>The <typeparamref name="T"/>.</returns>
|
|||
/// <exception cref="IndexOutOfRangeException">
|
|||
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
|||
/// </exception>
|
|||
public ref T this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
#if DEBUG
|
|||
if (index.CompareTo(0) < 0 || index.CompareTo(_size) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
} |
|||
#endif
|
|||
|
|||
return ref _data![index]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Appends a given number of empty items to the array returning
|
|||
/// the items as a slice.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of items in the slice.</param>
|
|||
/// <param name="clear">Whether to clear the new slice, Defaults to <see langword="true"/>.</param>
|
|||
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
|||
public ArraySlice<T> Add(int length, bool clear = true) |
|||
{ |
|||
var position = _size; |
|||
|
|||
// Expand the array.
|
|||
Length += length; |
|||
|
|||
var slice = AsSlice(position, Length - position); |
|||
|
|||
if (clear) |
|||
{ |
|||
slice.Span.Clear(); |
|||
} |
|||
|
|||
return slice; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Appends the slice to the array copying the data across.
|
|||
/// </summary>
|
|||
/// <param name="value">The array slice.</param>
|
|||
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
|||
public ArraySlice<T> Add(in ReadOnlySlice<T> value) |
|||
{ |
|||
var position = _size; |
|||
|
|||
// Expand the array.
|
|||
Length += value.Length; |
|||
|
|||
var slice = AsSlice(position, Length - position); |
|||
|
|||
value.Span.CopyTo(slice.Span); |
|||
|
|||
return slice; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the array.
|
|||
/// Allocated memory is left intact for future usage.
|
|||
/// </summary>
|
|||
public void Clear() |
|||
{ |
|||
// No need to actually clear since we're not allowing reference types.
|
|||
_size = 0; |
|||
} |
|||
|
|||
private void EnsureCapacity(int min) |
|||
{ |
|||
var length = _data?.Length ?? 0; |
|||
|
|||
if (length >= min) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Same expansion algorithm as List<T>.
|
|||
var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; |
|||
|
|||
if (newCapacity > MaxCoreClrArrayLength) |
|||
{ |
|||
newCapacity = MaxCoreClrArrayLength; |
|||
} |
|||
|
|||
if (newCapacity < min) |
|||
{ |
|||
newCapacity = (uint)min; |
|||
} |
|||
|
|||
var array = new T[newCapacity]; |
|||
|
|||
if (_size > 0) |
|||
{ |
|||
Array.Copy(_data!, array, _size); |
|||
} |
|||
|
|||
_data = array; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the current state of the array as a slice.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public ArraySlice<T> AsSlice() => AsSlice(Length); |
|||
|
|||
/// <summary>
|
|||
/// Returns the current state of the array as a slice.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of items in the slice.</param>
|
|||
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public ArraySlice<T> AsSlice(int length) => new ArraySlice<T>(_data!, 0, length); |
|||
|
|||
/// <summary>
|
|||
/// Returns the current state of the array as a slice.
|
|||
/// </summary>
|
|||
/// <param name="start">The index at which to begin the slice.</param>
|
|||
/// <param name="length">The number of items in the slice.</param>
|
|||
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length); |
|||
} |
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// Ported from: https://github.com/SixLabors/Fonts/
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// ArraySlice represents a contiguous region of arbitrary memory similar
|
|||
/// to <see cref="Memory{T}"/> and <see cref="Span{T}"/> though constrained
|
|||
/// to arrays.
|
|||
/// Unlike <see cref="Span{T}"/>, it is not a byref-like type.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of item contained in the slice.</typeparam>
|
|||
internal readonly struct ArraySlice<T> : IReadOnlyList<T> |
|||
where T : struct |
|||
{ |
|||
/// <summary>
|
|||
/// Gets an empty <see cref="ArraySlice{T}"/>
|
|||
/// </summary>
|
|||
public static ArraySlice<T> Empty => new ArraySlice<T>(Array.Empty<T>()); |
|||
|
|||
private readonly T[] _data; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="data">The underlying data buffer.</param>
|
|||
public ArraySlice(T[] data) |
|||
: this(data, 0, data.Length) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="data">The underlying data buffer.</param>
|
|||
/// <param name="start">The offset position in the underlying buffer this slice was created from.</param>
|
|||
/// <param name="length">The number of items in the slice.</param>
|
|||
public ArraySlice(T[] data, int start, int length) |
|||
{ |
|||
#if DEBUG
|
|||
if (start.CompareTo(0) < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(start)); |
|||
} |
|||
|
|||
if (length.CompareTo(data.Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
if ((start + length).CompareTo(data.Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(data)); |
|||
} |
|||
#endif
|
|||
|
|||
_data = data; |
|||
Start = start; |
|||
Length = length; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Gets a value that indicates whether this instance of <see cref="ArraySlice{T}"/> is Empty.
|
|||
/// </summary>
|
|||
public bool IsEmpty => Length == 0; |
|||
|
|||
/// <summary>
|
|||
/// Gets the offset position in the underlying buffer this slice was created from.
|
|||
/// </summary>
|
|||
public int Start { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items in the slice.
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a <see cref="Span{T}"/> representing this slice.
|
|||
/// </summary>
|
|||
public Span<T> Span => new Span<T>(_data, Start, Length); |
|||
|
|||
/// <summary>
|
|||
/// Returns a reference to specified element of the slice.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the element to return.</param>
|
|||
/// <returns>The <typeparamref name="T"/>.</returns>
|
|||
/// <exception cref="IndexOutOfRangeException">
|
|||
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
|||
/// </exception>
|
|||
public ref T this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
#if DEBUG
|
|||
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
} |
|||
#endif
|
|||
var i = index + Start; |
|||
|
|||
return ref _data[i]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
|
|||
/// </summary>
|
|||
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice) |
|||
{ |
|||
return new ReadOnlySlice<T>(slice._data).AsSlice(slice.Start, slice.Length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
|
|||
/// </summary>
|
|||
public static implicit operator ArraySlice<T>(T[] array) => new ArraySlice<T>(array, 0, array.Length); |
|||
|
|||
/// <summary>
|
|||
/// Fills the contents of this slice with the given value.
|
|||
/// </summary>
|
|||
public void Fill(T value) => Span.Fill(value); |
|||
|
|||
/// <summary>
|
|||
/// Forms a slice out of the given slice, beginning at 'start', of given length
|
|||
/// </summary>
|
|||
/// <param name="start">The index at which to begin this slice.</param>
|
|||
/// <param name="length">The desired length for the slice (exclusive).</param>
|
|||
/// <exception cref="ArgumentOutOfRangeException">
|
|||
/// Thrown when the specified <paramref name="start"/> or end index is not in range (<0 or >Length).
|
|||
/// </exception>
|
|||
public ArraySlice<T> Slice(int start, int length) => new ArraySlice<T>(_data, 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="ArraySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
|
|||
public ArraySlice<T> Take(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new ArraySlice<T>(_data, 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="ArraySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
|
|||
public ArraySlice<T> Skip(int length) |
|||
{ |
|||
if (IsEmpty) |
|||
{ |
|||
return this; |
|||
} |
|||
|
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new ArraySlice<T>(_data, Start + length, Length - length); |
|||
} |
|||
|
|||
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator() => |
|||
new ImmutableReadOnlyListStructEnumerator<T>(this); |
|||
|
|||
/// <inheritdoc/>
|
|||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); |
|||
|
|||
/// <inheritdoc/>
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
/// <inheritdoc/>
|
|||
T IReadOnlyList<T>.this[int index] => this[index]; |
|||
|
|||
/// <inheritdoc/>
|
|||
int IReadOnlyCollection<T>.Count => Length; |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
// RichTextKit
|
|||
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
|
|||
//
|
|||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|||
// not use this product except in compliance with the License. You may obtain
|
|||
// a copy of the License at
|
|||
//
|
|||
// http://www.apache.org/licenses/LICENSE-2.0
|
|||
//
|
|||
// Unless required by applicable law or agreed to in writing, software
|
|||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|||
// License for the specific language governing permissions and limitations
|
|||
// under the License.
|
|||
// Copied from: https://github.com/toptensoftware/RichTextKit
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for binary searching an IReadOnlyList collection
|
|||
/// </summary>
|
|||
public static class BinarySearchExtension |
|||
{ |
|||
private static int GetMedian(int low, int hi) |
|||
{ |
|||
System.Diagnostics.Debug.Assert(low <= hi); |
|||
System.Diagnostics.Debug.Assert(hi - low >= 0, "Length overflow!"); |
|||
return low + (hi - low >> 1); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs a binary search on the entire contents of an IReadOnlyList
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The list element type</typeparam>
|
|||
/// <param name="list">The list to be searched</param>
|
|||
/// <param name="value">The value to search for</param>
|
|||
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
|
|||
public static int BinarySearch<T>(this IReadOnlyList<T> list, T value) where T : IComparable |
|||
{ |
|||
return list.BinarySearch(value, Comparer<T>.Default); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs a binary search on the entire contents of an IReadOnlyList
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The list element type</typeparam>
|
|||
/// <param name="list">The list to be searched</param>
|
|||
/// <param name="value">The value to search for</param>
|
|||
/// <param name="comparer">The comparer</param>
|
|||
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
|
|||
public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer) where T : IComparable |
|||
{ |
|||
return list.BinarySearch(0, list.Count, value, comparer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs a binary search on a a subset of an IReadOnlyList
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The list element type</typeparam>
|
|||
/// <typeparam name="U">The value type being searched for</typeparam>
|
|||
/// <param name="list">The list to be searched</param>
|
|||
/// <param name="index">The start of the range to be searched</param>
|
|||
/// <param name="length">The length of the range to be searched</param>
|
|||
/// <param name="value">The value to search for</param>
|
|||
/// <param name="comparer">A comparer</param>
|
|||
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
|
|||
public static int BinarySearch<T>(this IReadOnlyList<T> list, int index, int length, T value, IComparer<T> comparer) |
|||
{ |
|||
// Based on this: https://referencesource.microsoft.com/#mscorlib/system/array.cs,957
|
|||
var lo = index; |
|||
var hi = index + length - 1; |
|||
while (lo <= hi) |
|||
{ |
|||
var i = GetMedian(lo, hi); |
|||
var c = comparer.Compare(list[i], value); |
|||
if (c == 0) |
|||
return i; |
|||
if (c < 0) |
|||
{ |
|||
lo = i + 1; |
|||
} |
|||
else |
|||
{ |
|||
hi = i - 1; |
|||
} |
|||
} |
|||
return ~lo; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,58 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// Ported from: https://github.com/SixLabors/Fonts/
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a mapped view of an underlying slice, selecting arbitrary indices
|
|||
/// from the source array.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of item contained in the underlying array.</typeparam>
|
|||
internal readonly struct MappedArraySlice<T> |
|||
where T : struct |
|||
{ |
|||
private readonly ArraySlice<T> _data; |
|||
private readonly ArraySlice<int> _map; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="MappedArraySlice{T}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="data">The data slice.</param>
|
|||
/// <param name="map">The map slice.</param>
|
|||
public MappedArraySlice(in ArraySlice<T> data, in ArraySlice<int> map) |
|||
{ |
|||
#if DEBUG
|
|||
if (map.Length.CompareTo(data.Length) > 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(map)); |
|||
} |
|||
#endif
|
|||
|
|||
_data = data; |
|||
_map = map; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items in the map.
|
|||
/// </summary>
|
|||
public int Length => _map.Length; |
|||
|
|||
/// <summary>
|
|||
/// Returns a reference to specified element of the slice.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the element to return.</param>
|
|||
/// <returns>The <typeparamref name="T"/>.</returns>
|
|||
/// <exception cref="IndexOutOfRangeException">
|
|||
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
|
|||
/// </exception>
|
|||
public ref T this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get => ref _data[_map[index]]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,596 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
//+-----------------------------------------------------------------------
|
|||
//
|
|||
//
|
|||
//
|
|||
// Contents: Generic span types
|
|||
//
|
|||
// [As of this creation, C# has no real generic type system]
|
|||
//
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
internal class Span |
|||
{ |
|||
public readonly object? element; |
|||
public int length; |
|||
|
|||
public Span(object? element, int length) |
|||
{ |
|||
this.element = element; |
|||
this.length = length; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// VECTOR: A series of spans
|
|||
/// </summary>
|
|||
internal class SpanVector : IEnumerable |
|||
{ |
|||
private static readonly Equals s_referenceEquals = object.ReferenceEquals; |
|||
private static readonly Equals s_equals = object.Equals; |
|||
|
|||
private FrugalStructList<Span> _spans; |
|||
|
|||
internal SpanVector( |
|||
object? defaultObject, |
|||
FrugalStructList<Span> spans = new FrugalStructList<Span>()) |
|||
{ |
|||
Default = defaultObject; |
|||
_spans = spans; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Get enumerator to vector
|
|||
/// </summary>
|
|||
public IEnumerator GetEnumerator() |
|||
{ |
|||
return new SpanEnumerator(this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a new span to vector
|
|||
/// </summary>
|
|||
private void Add(Span span) |
|||
{ |
|||
_spans.Add(span); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Delete n elements of vector
|
|||
/// </summary>
|
|||
internal virtual void Delete(int index, int count, ref SpanPosition latestPosition) |
|||
{ |
|||
DeleteInternal(index, count); |
|||
|
|||
if (index <= latestPosition.Index) |
|||
latestPosition = new SpanPosition(); |
|||
} |
|||
|
|||
private void DeleteInternal(int index, int count) |
|||
{ |
|||
// Do removes highest index to lowest to minimize the number
|
|||
// of array entries copied.
|
|||
for (var i = index + count - 1; i >= index; --i) |
|||
{ |
|||
_spans.RemoveAt(i); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Insert n elements to vector
|
|||
/// </summary>
|
|||
private void Insert(int index, int count) |
|||
{ |
|||
for (var c = 0; c < count; c++) |
|||
_spans.Insert(index, new Span(null, 0)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Finds the span that contains the specified character position.
|
|||
/// </summary>
|
|||
/// <param name="cp">position to find</param>
|
|||
/// <param name="latestPosition">Position of the most recently accessed span (e.g., the current span
|
|||
/// of a SpanRider) for performance; FindSpan runs in O(1) time if the specified cp is in the same span
|
|||
/// or an adjacent span.</param>
|
|||
/// <param name="spanPosition">receives the index and first cp of the span that contains the specified
|
|||
/// position or, if the position is past the end of the vector, the index and cp just past the end of
|
|||
/// the last span.</param>
|
|||
/// <returns>Returns true if cp is in range or false if not.</returns>
|
|||
internal bool FindSpan(int cp, SpanPosition latestPosition, out SpanPosition spanPosition) |
|||
{ |
|||
Debug.Assert(cp >= 0); |
|||
|
|||
var spanCount = _spans.Count; |
|||
int spanIndex, spanCP; |
|||
|
|||
if (cp == 0) |
|||
{ |
|||
// CP zero always corresponds to span index zero
|
|||
spanIndex = 0; |
|||
spanCP = 0; |
|||
} |
|||
else if (cp >= latestPosition.Offset || cp * 2 < latestPosition.Offset) |
|||
{ |
|||
// One of the following is true:
|
|||
// 1. cp is after the latest position (the most recently accessed span)
|
|||
// 2. cp is closer to zero than to the latest position
|
|||
if (cp >= latestPosition.Offset) |
|||
{ |
|||
// case 1: scan forward from the latest position
|
|||
spanIndex = latestPosition.Index; |
|||
spanCP = latestPosition.Offset; |
|||
} |
|||
else |
|||
{ |
|||
// case 2: scan forward from the start of the span vector
|
|||
spanIndex = 0; |
|||
spanCP = 0; |
|||
} |
|||
|
|||
// Scan forward until we find the Span that contains the specified CP or
|
|||
// reach the end of the SpanVector
|
|||
for (; spanIndex < spanCount; ++spanIndex) |
|||
{ |
|||
var spanLength = _spans[spanIndex].length; |
|||
|
|||
if (cp < spanCP + spanLength) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
spanCP += spanLength; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// The specified CP is before the latest position but closer to it than to zero;
|
|||
// therefore scan backwards from the latest position
|
|||
spanIndex = latestPosition.Index; |
|||
spanCP = latestPosition.Offset; |
|||
|
|||
while (spanCP > cp) |
|||
{ |
|||
Debug.Assert(spanIndex > 0); |
|||
spanCP -= _spans[--spanIndex].length; |
|||
} |
|||
} |
|||
|
|||
// Return index and cp of span in out param.
|
|||
spanPosition = new SpanPosition(spanIndex, spanCP); |
|||
|
|||
// Return true if the span is in range.
|
|||
return spanIndex != spanCount; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Set an element as a value to a character range
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Implementation of span element object must implement Object.Equals to
|
|||
/// avoid runtime reflection cost on equality check of nested-type object.
|
|||
/// </remarks>
|
|||
public void SetValue(int first, int length, object element) |
|||
{ |
|||
Set(first, length, element, SpanVector.s_equals, new SpanPosition()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Set an element as a value to a character range; takes a SpanPosition of a recently accessed
|
|||
/// span for performance and returns a known valid SpanPosition
|
|||
/// </summary>
|
|||
public SpanPosition SetValue(int first, int length, object element, SpanPosition spanPosition) |
|||
{ |
|||
return Set(first, length, element, SpanVector.s_equals, spanPosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Set an element as a reference to a character range
|
|||
/// </summary>
|
|||
public void SetReference(int first, int length, object element) |
|||
{ |
|||
Set(first, length, element, SpanVector.s_referenceEquals, new SpanPosition()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Set an element as a reference to a character range; takes a SpanPosition of a recently accessed
|
|||
/// span for performance and returns a known valid SpanPosition
|
|||
/// </summary>
|
|||
public SpanPosition SetReference(int first, int length, object element, SpanPosition spanPosition) |
|||
{ |
|||
return Set(first, length, element, SpanVector.s_referenceEquals, spanPosition); |
|||
} |
|||
|
|||
private SpanPosition Set(int first, int length, object? element, Equals equals, SpanPosition spanPosition) |
|||
{ |
|||
var inRange = FindSpan(first, spanPosition, out spanPosition); |
|||
|
|||
// fs = index of first span partly or completely updated
|
|||
// fc = character index at start of fs
|
|||
var fs = spanPosition.Index; |
|||
var fc = spanPosition.Offset; |
|||
|
|||
// Find the span that contains the first affected cp
|
|||
if (!inRange) |
|||
{ |
|||
// The first cp is past the end of the last span
|
|||
if (fc < first) |
|||
{ |
|||
// Create default run up to first
|
|||
Add(new Span(Default, first - fc)); |
|||
} |
|||
|
|||
if (Count > 0 |
|||
&& equals(_spans[Count - 1].element, element)) |
|||
{ |
|||
// New Element matches end Element, just extend end Element
|
|||
_spans[Count - 1].length += length; |
|||
|
|||
// Make sure fs and fc still agree
|
|||
if (fs == Count) |
|||
{ |
|||
fc += length; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Add(new Span(element, length)); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Now find the last span affected by the update
|
|||
|
|||
var ls = fs; |
|||
var lc = fc; |
|||
while (ls < Count |
|||
&& lc + _spans[ls].length <= first + length) |
|||
{ |
|||
lc += _spans[ls].length; |
|||
ls++; |
|||
} |
|||
// ls = first span following update to remain unchanged in part or in whole
|
|||
// lc = character index at start of ls
|
|||
|
|||
// expand update region backwards to include existing Spans of identical
|
|||
// Element type
|
|||
|
|||
if (first == fc) |
|||
{ |
|||
// Item at [fs] is completely replaced. Check prior item
|
|||
|
|||
if (fs > 0 |
|||
&& equals(_spans[fs - 1].element, element)) |
|||
{ |
|||
// Expand update area over previous run of equal classification
|
|||
fs--; |
|||
fc -= _spans[fs].length; |
|||
first = fc; |
|||
length += _spans[fs].length; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Item at [fs] is partially replaced. Check if it is same as update
|
|||
if (equals(_spans[fs].element, element)) |
|||
{ |
|||
// Expand update area back to start of first affected equal valued run
|
|||
length = first + length - fc; |
|||
first = fc; |
|||
} |
|||
} |
|||
|
|||
// Expand update region forwards to include existing Spans of identical
|
|||
// Element type
|
|||
|
|||
if (ls < Count |
|||
&& equals(_spans[ls].element, element)) |
|||
{ |
|||
// Extend update region to end of existing split run
|
|||
|
|||
length = lc + _spans[ls].length - first; |
|||
lc += _spans[ls].length; |
|||
ls++; |
|||
} |
|||
|
|||
// If no old Spans remain beyond area affected by update, handle easily:
|
|||
|
|||
if (ls >= Count) |
|||
{ |
|||
// None of the old span list extended beyond the update region
|
|||
|
|||
if (fc < first) |
|||
{ |
|||
// Updated region leaves some of [fs]
|
|||
|
|||
if (Count != fs + 2) |
|||
{ |
|||
if (!Resize(fs + 2)) |
|||
throw new OutOfMemoryException(); |
|||
} |
|||
_spans[fs].length = first - fc; |
|||
_spans[fs + 1] = new Span(element, length); |
|||
} |
|||
else |
|||
{ |
|||
// Updated item replaces [fs]
|
|||
if (Count != fs + 1) |
|||
{ |
|||
if (!Resize(fs + 1)) |
|||
throw new OutOfMemoryException(); |
|||
} |
|||
_spans[fs] = new Span(element, length); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Record partial element type at end, if any
|
|||
|
|||
object? trailingElement = null; |
|||
var trailingLength = 0; |
|||
|
|||
if (first + length > lc) |
|||
{ |
|||
trailingElement = _spans[ls].element; |
|||
trailingLength = lc + _spans[ls].length - (first + length); |
|||
} |
|||
|
|||
// Calculate change in number of Spans
|
|||
|
|||
var spanDelta = 1 // The new span
|
|||
+ (first > fc ? 1 : 0) // part span at start
|
|||
- (ls - fs); // existing affected span count
|
|||
|
|||
// Note part span at end doesn't affect the calculation - the run may need
|
|||
// updating, but it doesn't need creating.
|
|||
|
|||
if (spanDelta < 0) |
|||
{ |
|||
DeleteInternal(fs + 1, -spanDelta); |
|||
} |
|||
else if (spanDelta > 0) |
|||
{ |
|||
Insert(fs + 1, spanDelta); |
|||
// Initialize inserted Spans
|
|||
for (var i = 0; i < spanDelta; i++) |
|||
{ |
|||
_spans[fs + 1 + i] = new Span(null, 0); |
|||
} |
|||
} |
|||
|
|||
// Assign Element values
|
|||
|
|||
// Correct Length of split span before updated range
|
|||
|
|||
if (fc < first) |
|||
{ |
|||
_spans[fs].length = first - fc; |
|||
fs++; |
|||
fc = first; |
|||
} |
|||
|
|||
// Record Element type for updated range
|
|||
|
|||
_spans[fs] = new Span(element, length); |
|||
fs++; |
|||
fc += length; |
|||
|
|||
// Correct Length of split span following updated range
|
|||
|
|||
if (lc < first + length) |
|||
{ |
|||
_spans[fs] = new Span(trailingElement, trailingLength); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Return a known valid span position.
|
|||
return new SpanPosition(fs, fc); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Number of spans in vector
|
|||
/// </summary>
|
|||
public int Count |
|||
{ |
|||
get { return _spans.Count; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The default element of vector
|
|||
/// </summary>
|
|||
public object? Default { get; } |
|||
|
|||
/// <summary>
|
|||
/// Span accessor at nth element
|
|||
/// </summary>
|
|||
public Span this[int index] |
|||
{ |
|||
get { return _spans[index]; } |
|||
} |
|||
|
|||
private bool Resize(int targetCount) |
|||
{ |
|||
if (targetCount > Count) |
|||
{ |
|||
for (var c = 0; c < targetCount - Count; c++) |
|||
{ |
|||
_spans.Add(new Span(null, 0)); |
|||
} |
|||
} |
|||
else if (targetCount < Count) |
|||
{ |
|||
DeleteInternal(targetCount, Count - targetCount); |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Equality check method
|
|||
/// </summary>
|
|||
internal delegate bool Equals(object? first, object? second); |
|||
|
|||
/// <summary>
|
|||
/// ENUMERATOR: To navigate a vector through its element
|
|||
/// </summary>
|
|||
internal sealed class SpanEnumerator : IEnumerator |
|||
{ |
|||
private readonly SpanVector _spans; |
|||
private int _current; // current span
|
|||
|
|||
internal SpanEnumerator(SpanVector spans) |
|||
{ |
|||
_spans = spans; |
|||
_current = -1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The current span
|
|||
/// </summary>
|
|||
public object Current |
|||
{ |
|||
get { return _spans[_current]; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Move to the next span
|
|||
/// </summary>
|
|||
public bool MoveNext() |
|||
{ |
|||
_current++; |
|||
|
|||
return _current < _spans.Count; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reset the enumerator
|
|||
/// </summary>
|
|||
public void Reset() |
|||
{ |
|||
_current = -1; |
|||
} |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Represents a Span's position as a pair of related values: its index in the
|
|||
/// SpanVector its CP offset from the start of the SpanVector.
|
|||
/// </summary>
|
|||
internal readonly struct SpanPosition |
|||
{ |
|||
internal SpanPosition(int spanIndex, int spanOffset) |
|||
{ |
|||
Index = spanIndex; |
|||
Offset = spanOffset; |
|||
} |
|||
|
|||
internal int Index { get; } |
|||
|
|||
internal int Offset { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// RIDER: To navigate a vector through character index
|
|||
/// </summary>
|
|||
internal struct SpanRider |
|||
{ |
|||
private readonly SpanVector _spans; // vector of spans
|
|||
private SpanPosition _spanPosition; // index and cp of current span
|
|||
|
|||
public SpanRider(SpanVector spans, SpanPosition latestPosition) : this(spans, latestPosition, latestPosition.Offset) |
|||
{ |
|||
} |
|||
|
|||
public SpanRider(SpanVector spans, SpanPosition latestPosition = new SpanPosition(), int cp = 0) |
|||
{ |
|||
_spans = spans; |
|||
_spanPosition = new SpanPosition(); |
|||
CurrentPosition = 0; |
|||
Length = 0; |
|||
At(latestPosition, cp); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Move rider to a given cp
|
|||
/// </summary>
|
|||
public bool At(int cp) |
|||
{ |
|||
return At(_spanPosition, cp); |
|||
} |
|||
|
|||
public bool At(SpanPosition latestPosition, int cp) |
|||
{ |
|||
var inRange = _spans.FindSpan(cp, latestPosition, out _spanPosition); |
|||
if (inRange) |
|||
{ |
|||
// cp is in range:
|
|||
// - Length is the distance to the end of the span
|
|||
// - CurrentPosition is cp
|
|||
Length = _spans[_spanPosition.Index].length - (cp - _spanPosition.Offset); |
|||
CurrentPosition = cp; |
|||
} |
|||
else |
|||
{ |
|||
// cp is out of range:
|
|||
// - Length is the default span length
|
|||
// - CurrentPosition is the end of the last span
|
|||
Length = int.MaxValue; |
|||
CurrentPosition = _spanPosition.Offset; |
|||
} |
|||
|
|||
return inRange; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The first cp of the current span
|
|||
/// </summary>
|
|||
public int CurrentSpanStart |
|||
{ |
|||
get { return _spanPosition.Offset; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The length of current span start from the current cp
|
|||
/// </summary>
|
|||
public int Length { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// The current position
|
|||
/// </summary>
|
|||
public int CurrentPosition { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// The element of the current span
|
|||
/// </summary>
|
|||
public object? CurrentElement |
|||
{ |
|||
get { return _spanPosition.Index >= _spans.Count ? _spans.Default : _spans[_spanPosition.Index].element; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Index of the span at the current position.
|
|||
/// </summary>
|
|||
public int CurrentSpanIndex |
|||
{ |
|||
get { return _spanPosition.Index; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Index and first cp of the current span.
|
|||
/// </summary>
|
|||
public SpanPosition SpanPosition |
|||
{ |
|||
get { return _spanPosition; } |
|||
} |
|||
} |
|||
} |
|||
@ -1,838 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia formatted text implementation.
|
|||
/// </summary>
|
|||
internal class FormattedTextImpl : IFormattedTextImpl |
|||
{ |
|||
private static readonly ThreadLocal<SKTextBlobBuilder> t_builder = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder()); |
|||
|
|||
private const float MAX_LINE_WIDTH = 10000; |
|||
|
|||
private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes = |
|||
new List<KeyValuePair<FBrushRange, IBrush>>(); |
|||
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>(); |
|||
private readonly SKPaint _paint; |
|||
private readonly List<Rect> _rects = new List<Rect>(); |
|||
public string Text { get; } |
|||
private readonly TextWrapping _wrapping; |
|||
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); |
|||
private float _lineHeight = 0; |
|||
private float _lineOffset = 0; |
|||
private Rect _bounds; |
|||
private List<AvaloniaFormattedTextLine> _skiaLines; |
|||
private ReadOnlySlice<ushort> _glyphs; |
|||
private ReadOnlySlice<float> _advances; |
|||
|
|||
public FormattedTextImpl( |
|||
string text, |
|||
Typeface typeface, |
|||
double fontSize, |
|||
TextAlignment textAlignment, |
|||
TextWrapping wrapping, |
|||
Size constraint, |
|||
IReadOnlyList<FormattedTextStyleSpan> spans) |
|||
{ |
|||
Text = text ?? string.Empty; |
|||
|
|||
UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize); |
|||
|
|||
_paint = new SKPaint |
|||
{ |
|||
TextEncoding = SKTextEncoding.Utf16, |
|||
IsStroke = false, |
|||
IsAntialias = true, |
|||
LcdRenderText = true, |
|||
SubpixelText = true, |
|||
IsLinearText = true, |
|||
Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface, |
|||
TextSize = (float)fontSize, |
|||
TextAlign = textAlignment.ToSKTextAlign() |
|||
}; |
|||
|
|||
//currently Skia does not measure properly with Utf8 !!!
|
|||
//Paint.TextEncoding = SKTextEncoding.Utf8;
|
|||
|
|||
_wrapping = wrapping; |
|||
_constraint = constraint; |
|||
|
|||
if (spans != null) |
|||
{ |
|||
foreach (var span in spans) |
|||
{ |
|||
if (span.ForegroundBrush != null) |
|||
{ |
|||
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Rebuild(); |
|||
} |
|||
|
|||
public Size Constraint => _constraint; |
|||
|
|||
public Rect Bounds => _bounds; |
|||
|
|||
public IEnumerable<FormattedTextLine> GetLines() |
|||
{ |
|||
return _lines; |
|||
} |
|||
|
|||
public TextHitTestResult HitTestPoint(Point point) |
|||
{ |
|||
float y = (float)point.Y; |
|||
|
|||
AvaloniaFormattedTextLine line = default; |
|||
|
|||
float nextTop = 0; |
|||
|
|||
foreach(var currentLine in _skiaLines) |
|||
{ |
|||
if(currentLine.Top <= y) |
|||
{ |
|||
line = currentLine; |
|||
nextTop = currentLine.Top + currentLine.Height; |
|||
} |
|||
else |
|||
{ |
|||
nextTop = currentLine.Top; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!line.Equals(default(AvaloniaFormattedTextLine))) |
|||
{ |
|||
var rects = GetRects(); |
|||
|
|||
for (int c = line.Start; c < line.Start + line.TextLength; c++) |
|||
{ |
|||
var rc = rects[c]; |
|||
if (rc.Contains(point)) |
|||
{ |
|||
return new TextHitTestResult |
|||
{ |
|||
IsInside = !(line.TextLength > line.Length), |
|||
TextPosition = c, |
|||
IsTrailing = (point.X - rc.X) > rc.Width / 2 |
|||
}; |
|||
} |
|||
} |
|||
|
|||
int offset = 0; |
|||
|
|||
if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0) |
|||
{ |
|||
offset = line.TextLength > line.Length ? |
|||
line.Length : (line.Length - 1); |
|||
} |
|||
|
|||
if (y < nextTop) |
|||
{ |
|||
return new TextHitTestResult |
|||
{ |
|||
IsInside = false, |
|||
TextPosition = line.Start + offset, |
|||
IsTrailing = Text.Length == (line.Start + offset + 1) |
|||
}; |
|||
} |
|||
} |
|||
|
|||
bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); |
|||
|
|||
return new TextHitTestResult() |
|||
{ |
|||
IsInside = false, |
|||
IsTrailing = end, |
|||
TextPosition = end ? Text.Length - 1 : 0 |
|||
}; |
|||
} |
|||
|
|||
public Rect HitTestTextPosition(int index) |
|||
{ |
|||
if (string.IsNullOrEmpty(Text)) |
|||
{ |
|||
var alignmentOffset = TransformX(0, 0, _paint.TextAlign); |
|||
return new Rect(alignmentOffset, 0, 0, _lineHeight); |
|||
} |
|||
var rects = GetRects(); |
|||
if (index >= Text.Length || index < 0) |
|||
{ |
|||
var r = rects.LastOrDefault(); |
|||
|
|||
var c = Text[Text.Length - 1]; |
|||
|
|||
switch (c) |
|||
{ |
|||
case '\n': |
|||
case '\r': |
|||
return new Rect(r.X, r.Y, 0, _lineHeight); |
|||
default: |
|||
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); |
|||
} |
|||
} |
|||
return rects[index]; |
|||
} |
|||
|
|||
public IEnumerable<Rect> HitTestTextRange(int index, int length) |
|||
{ |
|||
List<Rect> result = new List<Rect>(); |
|||
|
|||
var rects = GetRects(); |
|||
|
|||
int lastIndex = index + length - 1; |
|||
|
|||
foreach (var line in _skiaLines.Where(l => |
|||
(l.Start + l.Length) > index && |
|||
lastIndex >= l.Start && |
|||
!l.IsEmptyTrailingLine)) |
|||
{ |
|||
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0); |
|||
|
|||
double left = rects[line.Start > index ? line.Start : index].X; |
|||
double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right; |
|||
|
|||
result.Add(new Rect(left, line.Top, right - left, line.Height)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return Text; |
|||
} |
|||
|
|||
private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) |
|||
{ |
|||
if(length == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var glyphs = _glyphs.Buffer.Span.Slice(start, length); |
|||
var advances = _advances.Buffer.Span.Slice(start, length); |
|||
var builder = t_builder.Value; |
|||
|
|||
var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0); |
|||
|
|||
buffer.SetGlyphs(glyphs); |
|||
|
|||
var positions = buffer.GetPositionSpan(); |
|||
|
|||
var pos = 0f; |
|||
|
|||
for (int i = 0; i < advances.Length; i++) |
|||
{ |
|||
positions[i] = pos; |
|||
|
|||
pos += advances[i]; |
|||
} |
|||
|
|||
var blob = builder.Build(); |
|||
|
|||
if(blob != null) |
|||
{ |
|||
canvas.DrawText(blob, x, y, paint); |
|||
} |
|||
} |
|||
|
|||
internal void Draw(DrawingContextImpl context, |
|||
SKCanvas canvas, |
|||
SKPoint origin, |
|||
DrawingContextImpl.PaintWrapper foreground, |
|||
bool canUseLcdRendering) |
|||
{ |
|||
/* TODO: This originated from Native code, it might be useful for debugging character positions as |
|||
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when |
|||
* not needed anymore. |
|||
|
|||
SkPaint dpaint; |
|||
ctx->Canvas->save(); |
|||
ctx->Canvas->translate(origin.fX, origin.fY); |
|||
for (int c = 0; c < Lines.size(); c++) |
|||
{ |
|||
dpaint.setARGB(255, 0, 0, 0); |
|||
SkRect rc; |
|||
rc.fLeft = 0; |
|||
rc.fTop = Lines[c].Top; |
|||
rc.fRight = Lines[c].Width; |
|||
rc.fBottom = rc.fTop + LineOffset; |
|||
ctx->Canvas->drawRect(rc, dpaint); |
|||
} |
|||
for (int c = 0; c < Length; c++) |
|||
{ |
|||
dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10); |
|||
dpaint.setStyle(SkPaint::kFill_Style); |
|||
ctx->Canvas->drawRect(Rects[c], dpaint); |
|||
} |
|||
ctx->Canvas->restore(); |
|||
*/ |
|||
using (var paint = _paint.Clone()) |
|||
{ |
|||
IDisposable currd = null; |
|||
var currentWrapper = foreground; |
|||
SKPaint currentPaint = null; |
|||
try |
|||
{ |
|||
ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering); |
|||
bool hasCusomFGBrushes = _foregroundBrushes.Any(); |
|||
|
|||
for (int c = 0; c < _skiaLines.Count; c++) |
|||
{ |
|||
AvaloniaFormattedTextLine line = _skiaLines[c]; |
|||
|
|||
float x = TransformX(origin.X, line.Width, paint.TextAlign); |
|||
|
|||
if (!hasCusomFGBrushes) |
|||
{ |
|||
DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint); |
|||
} |
|||
else |
|||
{ |
|||
float currX = x; |
|||
float measure; |
|||
int len; |
|||
float factor; |
|||
|
|||
switch (paint.TextAlign) |
|||
{ |
|||
case SKTextAlign.Left: |
|||
factor = 0; |
|||
break; |
|||
case SKTextAlign.Center: |
|||
factor = 0.5f; |
|||
break; |
|||
case SKTextAlign.Right: |
|||
factor = 1; |
|||
break; |
|||
default: |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
|
|||
currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; |
|||
|
|||
for (int i = line.Start; i < line.Start + line.Length;) |
|||
{ |
|||
var fb = GetNextForegroundBrush(ref line, i, out len); |
|||
|
|||
if (fb != null) |
|||
{ |
|||
//TODO: figure out how to get the brush size
|
|||
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, |
|||
new Size()); |
|||
} |
|||
else |
|||
{ |
|||
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|||
currentWrapper = foreground; |
|||
} |
|||
|
|||
measure = MeasureText(i, len); |
|||
currX += measure * factor; |
|||
|
|||
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); |
|||
|
|||
DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint); |
|||
|
|||
i += len; |
|||
currX += measure * (1 - factor); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|||
currd?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper, |
|||
ref IDisposable curr, SKPaint paint, bool canUseLcdRendering) |
|||
{ |
|||
if (current == wrapper.Paint) |
|||
return; |
|||
curr?.Dispose(); |
|||
curr = wrapper.ApplyTo(paint); |
|||
paint.LcdRenderText = canUseLcdRendering; |
|||
} |
|||
|
|||
private static bool IsBreakChar(char c) |
|||
{ |
|||
//white space or zero space whitespace
|
|||
return char.IsWhiteSpace(c) || c == '\u200B'; |
|||
} |
|||
|
|||
private static int LineBreak(string textInput, int textIndex, int stop, |
|||
SKPaint paint, float maxWidth, |
|||
out int trailingCount) |
|||
{ |
|||
int lengthBreak; |
|||
if (maxWidth == -1) |
|||
{ |
|||
lengthBreak = stop - textIndex; |
|||
} |
|||
else |
|||
{ |
|||
string subText = textInput.Substring(textIndex, stop - textIndex); |
|||
lengthBreak = (int)paint.BreakText(subText, maxWidth, out _); |
|||
} |
|||
|
|||
//Check for white space or line breakers before the lengthBreak
|
|||
int startIndex = textIndex; |
|||
int index = textIndex; |
|||
int word_start = textIndex; |
|||
bool prevBreak = true; |
|||
|
|||
trailingCount = 0; |
|||
|
|||
while (index < stop) |
|||
{ |
|||
int prevText = index; |
|||
char currChar = textInput[index++]; |
|||
bool currBreak = IsBreakChar(currChar); |
|||
|
|||
if (!currBreak && prevBreak) |
|||
{ |
|||
word_start = prevText; |
|||
} |
|||
|
|||
prevBreak = currBreak; |
|||
|
|||
if (index > startIndex + lengthBreak) |
|||
{ |
|||
if (currBreak) |
|||
{ |
|||
// eat the rest of the whitespace
|
|||
while (index < stop && IsBreakChar(textInput[index])) |
|||
{ |
|||
index++; |
|||
} |
|||
|
|||
trailingCount = index - prevText; |
|||
} |
|||
else |
|||
{ |
|||
// backup until a whitespace (or 1 char)
|
|||
if (word_start == startIndex) |
|||
{ |
|||
if (prevText > startIndex) |
|||
{ |
|||
index = prevText; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
index = word_start; |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
|
|||
if ('\n' == currChar) |
|||
{ |
|||
int ret = index - startIndex; |
|||
int lineBreakSize = 1; |
|||
if (index < stop) |
|||
{ |
|||
currChar = textInput[index++]; |
|||
if ('\r' == currChar) |
|||
{ |
|||
ret = index - startIndex; |
|||
++lineBreakSize; |
|||
} |
|||
} |
|||
|
|||
trailingCount = lineBreakSize; |
|||
|
|||
return ret; |
|||
} |
|||
|
|||
if ('\r' == currChar) |
|||
{ |
|||
int ret = index - startIndex; |
|||
int lineBreakSize = 1; |
|||
if (index < stop) |
|||
{ |
|||
currChar = textInput[index++]; |
|||
if ('\n' == currChar) |
|||
{ |
|||
ret = index - startIndex; |
|||
++lineBreakSize; |
|||
} |
|||
} |
|||
|
|||
trailingCount = lineBreakSize; |
|||
|
|||
return ret; |
|||
} |
|||
} |
|||
|
|||
return index - startIndex; |
|||
} |
|||
|
|||
private void BuildRects() |
|||
{ |
|||
// Build character rects
|
|||
SKTextAlign align = _paint.TextAlign; |
|||
|
|||
for (int li = 0; li < _skiaLines.Count; li++) |
|||
{ |
|||
var line = _skiaLines[li]; |
|||
float prevRight = TransformX(0, line.Width, align); |
|||
double nextTop = line.Top + line.Height; |
|||
|
|||
if (li + 1 < _skiaLines.Count) |
|||
{ |
|||
nextTop = _skiaLines[li + 1].Top; |
|||
} |
|||
|
|||
for (int i = line.Start; i < line.Start + line.TextLength; i++) |
|||
{ |
|||
var w = line.IsEmptyTrailingLine ? 0 : _advances[i]; |
|||
|
|||
_rects.Add(new Rect( |
|||
prevRight, |
|||
line.Top, |
|||
w, |
|||
nextTop - line.Top)); |
|||
prevRight += w; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length) |
|||
{ |
|||
IBrush result = null; |
|||
int len = length = line.Start + line.Length - index; |
|||
|
|||
if (_foregroundBrushes.Any()) |
|||
{ |
|||
var bi = _foregroundBrushes.FindIndex(b => |
|||
b.Key.StartIndex <= index && |
|||
b.Key.EndIndex > index |
|||
); |
|||
|
|||
if (bi > -1) |
|||
{ |
|||
var match = _foregroundBrushes[bi]; |
|||
|
|||
len = match.Key.EndIndex - index; |
|||
result = match.Value; |
|||
|
|||
if (len > 0 && len < length) |
|||
{ |
|||
length = len; |
|||
} |
|||
} |
|||
|
|||
int endIndex = index + length; |
|||
int max = bi == -1 ? _foregroundBrushes.Count : bi; |
|||
var next = _foregroundBrushes.Take(max) |
|||
.Where(b => b.Key.StartIndex < endIndex && |
|||
b.Key.StartIndex > index) |
|||
.OrderBy(b => b.Key.StartIndex) |
|||
.FirstOrDefault(); |
|||
|
|||
if (next.Value != null) |
|||
{ |
|||
length = next.Key.StartIndex - index; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private List<Rect> GetRects() |
|||
{ |
|||
if (Text.Length > _rects.Count) |
|||
{ |
|||
BuildRects(); |
|||
} |
|||
|
|||
return _rects; |
|||
} |
|||
|
|||
private void Rebuild() |
|||
{ |
|||
var length = Text.Length; |
|||
|
|||
_lines.Clear(); |
|||
_rects.Clear(); |
|||
_skiaLines = new List<AvaloniaFormattedTextLine>(); |
|||
|
|||
int curOff = 0; |
|||
float curY = 0; |
|||
|
|||
var metrics = _paint.FontMetrics; |
|||
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
|
|||
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
|
|||
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
|
|||
var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
|
|||
var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
|
|||
var lastLineDescent = mBottom - mDescent; |
|||
|
|||
// This seems like the best measure of full vertical extent
|
|||
// matches Direct2D line height
|
|||
_lineHeight = mDescent - mAscent + metrics.Leading; |
|||
|
|||
// Rendering is relative to baseline
|
|||
_lineOffset = (-metrics.Ascent); |
|||
|
|||
string subString; |
|||
|
|||
float widthConstraint = double.IsPositiveInfinity(_constraint.Width) |
|||
? -1 |
|||
: (float)_constraint.Width; |
|||
|
|||
while(curOff < length) |
|||
{ |
|||
float lineWidth = -1; |
|||
int measured; |
|||
int trailingnumber = 0; |
|||
|
|||
float constraint = -1; |
|||
|
|||
if (_wrapping == TextWrapping.Wrap) |
|||
{ |
|||
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; |
|||
if (constraint > MAX_LINE_WIDTH) |
|||
constraint = MAX_LINE_WIDTH; |
|||
} |
|||
|
|||
measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); |
|||
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); |
|||
line.Start = curOff; |
|||
line.TextLength = measured; |
|||
subString = Text.Substring(line.Start, line.TextLength); |
|||
lineWidth = MeasureText(line.Start, line.TextLength); |
|||
line.Length = measured - trailingnumber; |
|||
line.Width = lineWidth; |
|||
line.Height = _lineHeight; |
|||
line.Top = curY; |
|||
|
|||
_skiaLines.Add(line); |
|||
|
|||
curY += _lineHeight; |
|||
curY += mLeading; |
|||
curOff += measured; |
|||
|
|||
//if this is the last line and there are trailing newline characters then
|
|||
//insert a additional line
|
|||
if (curOff >= length) |
|||
{ |
|||
var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); |
|||
var lengthDiff = subString.Length - subStringMinusNewlines.Length; |
|||
if (lengthDiff > 0) |
|||
{ |
|||
AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); |
|||
lastLine.TextLength = lengthDiff; |
|||
lastLine.Start = curOff - lengthDiff; |
|||
var lastLineWidth = MeasureText(line.Start, line.TextLength); |
|||
lastLine.Length = 0; |
|||
lastLine.Width = lastLineWidth; |
|||
lastLine.Height = _lineHeight; |
|||
lastLine.Top = curY; |
|||
lastLine.IsEmptyTrailingLine = true; |
|||
|
|||
_skiaLines.Add(lastLine); |
|||
|
|||
curY += _lineHeight; |
|||
curY += mLeading; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Now convert to Avalonia data formats
|
|||
_lines.Clear(); |
|||
float maxX = 0; |
|||
|
|||
for (var c = 0; c < _skiaLines.Count; c++) |
|||
{ |
|||
var w = _skiaLines[c].Width; |
|||
if (maxX < w) |
|||
maxX = w; |
|||
|
|||
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height)); |
|||
} |
|||
|
|||
if (_skiaLines.Count == 0) |
|||
{ |
|||
_lines.Add(new FormattedTextLine(0, _lineHeight)); |
|||
_bounds = new Rect(0, 0, 0, _lineHeight); |
|||
} |
|||
else |
|||
{ |
|||
var lastLine = _skiaLines[_skiaLines.Count - 1]; |
|||
_bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height); |
|||
|
|||
if (double.IsPositiveInfinity(Constraint.Width)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
switch (_paint.TextAlign) |
|||
{ |
|||
case SKTextAlign.Center: |
|||
_bounds = new Rect(Constraint).CenterRect(_bounds); |
|||
break; |
|||
case SKTextAlign.Right: |
|||
_bounds = new Rect( |
|||
Constraint.Width - _bounds.Width, |
|||
0, |
|||
_bounds.Width, |
|||
_bounds.Height); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private float MeasureText(int start, int length) |
|||
{ |
|||
var width = 0f; |
|||
|
|||
for (int i = start; i < start + length; i++) |
|||
{ |
|||
var advance = _advances[i]; |
|||
|
|||
width += advance; |
|||
} |
|||
|
|||
return width; |
|||
} |
|||
|
|||
private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize) |
|||
{ |
|||
var glyphs = new ushort[text.Length]; |
|||
var advances = new float[text.Length]; |
|||
|
|||
var scale = fontSize / glyphTypeface.DesignEmHeight; |
|||
var width = 0f; |
|||
var characters = text.AsSpan(); |
|||
|
|||
for (int i = 0; i < characters.Length; i++) |
|||
{ |
|||
var c = characters[i]; |
|||
float advance; |
|||
ushort glyph; |
|||
|
|||
switch (c) |
|||
{ |
|||
case (char)0: |
|||
{ |
|||
glyph = glyphTypeface.GetGlyph(0x200B); |
|||
advance = 0; |
|||
break; |
|||
} |
|||
case '\t': |
|||
{ |
|||
glyph = glyphTypeface.GetGlyph(' '); |
|||
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4; |
|||
break; |
|||
} |
|||
default: |
|||
{ |
|||
glyph = glyphTypeface.GetGlyph(c); |
|||
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
glyphs[i] = glyph; |
|||
advances[i] = advance; |
|||
|
|||
width += advance; |
|||
} |
|||
|
|||
_glyphs = new ReadOnlySlice<ushort>(glyphs); |
|||
_advances = new ReadOnlySlice<float>(advances); |
|||
} |
|||
|
|||
private float TransformX(float originX, float lineWidth, SKTextAlign align) |
|||
{ |
|||
float x = 0; |
|||
|
|||
if (align == SKTextAlign.Left) |
|||
{ |
|||
x = originX; |
|||
} |
|||
else |
|||
{ |
|||
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ? |
|||
Constraint.Width : |
|||
_bounds.Width; |
|||
|
|||
switch (align) |
|||
{ |
|||
case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break; |
|||
case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break; |
|||
} |
|||
} |
|||
|
|||
return x; |
|||
} |
|||
|
|||
private void SetForegroundBrush(IBrush brush, int startIndex, int length) |
|||
{ |
|||
var key = new FBrushRange(startIndex, length); |
|||
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key)); |
|||
|
|||
if (index > -1) |
|||
{ |
|||
_foregroundBrushes.RemoveAt(index); |
|||
} |
|||
|
|||
if (brush != null) |
|||
{ |
|||
brush = brush.ToImmutable(); |
|||
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush)); |
|||
} |
|||
} |
|||
|
|||
private struct AvaloniaFormattedTextLine |
|||
{ |
|||
public float Height; |
|||
public int Length; |
|||
public int Start; |
|||
public int TextLength; |
|||
public float Top; |
|||
public float Width; |
|||
public bool IsEmptyTrailingLine; |
|||
}; |
|||
|
|||
private struct FBrushRange |
|||
{ |
|||
public FBrushRange(int startIndex, int length) |
|||
{ |
|||
StartIndex = startIndex; |
|||
Length = length; |
|||
} |
|||
|
|||
public int EndIndex => StartIndex + Length; |
|||
|
|||
public int Length { get; private set; } |
|||
|
|||
public int StartIndex { get; private set; } |
|||
|
|||
public bool Intersects(int index, int len) => |
|||
(index + len) > StartIndex && |
|||
(StartIndex + Length) > index; |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return $"{StartIndex}-{EndIndex}"; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,146 +1,139 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using HarfBuzzSharp; |
|||
using Buffer = HarfBuzzSharp.Buffer; |
|||
using GlyphInfo = HarfBuzzSharp.GlyphInfo; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal class TextShaperImpl : ITextShaperImpl |
|||
{ |
|||
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) |
|||
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, GlyphTypeface typeface, double fontRenderingEmSize, |
|||
CultureInfo culture, sbyte bidiLevel) |
|||
{ |
|||
using (var buffer = new Buffer()) |
|||
{ |
|||
FillBuffer(buffer, text); |
|||
|
|||
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); |
|||
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); |
|||
|
|||
MergeBreakPair(buffer); |
|||
|
|||
buffer.GuessSegmentProperties(); |
|||
|
|||
var glyphTypeface = typeface.GlyphTypeface; |
|||
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; |
|||
|
|||
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); |
|||
|
|||
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; |
|||
var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; |
|||
|
|||
font.Shape(buffer); |
|||
|
|||
if (buffer.Direction == Direction.RightToLeft) |
|||
{ |
|||
buffer.Reverse(); |
|||
} |
|||
|
|||
font.GetScale(out var scaleX, out _); |
|||
|
|||
var textScale = fontRenderingEmSize / scaleX; |
|||
|
|||
var bufferLength = buffer.Length; |
|||
|
|||
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); |
|||
|
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var glyphPositions = buffer.GetGlyphPositionSpan(); |
|||
|
|||
var glyphIndices = new ushort[bufferLength]; |
|||
|
|||
var clusters = new ushort[bufferLength]; |
|||
for (var i = 0; i < bufferLength; i++) |
|||
{ |
|||
var sourceInfo = glyphInfos[i]; |
|||
|
|||
double[] glyphAdvances = null; |
|||
var glyphIndex = (ushort)sourceInfo.Codepoint; |
|||
|
|||
Vector[] glyphOffsets = null; |
|||
var glyphCluster = (int)sourceInfo.Cluster; |
|||
|
|||
for (var i = 0; i < bufferLength; i++) |
|||
{ |
|||
glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; |
|||
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); |
|||
|
|||
clusters[i] = (ushort)glyphInfos[i].Cluster; |
|||
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); |
|||
|
|||
if (!glyphTypeface.IsFixedPitch) |
|||
{ |
|||
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); |
|||
} |
|||
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); |
|||
|
|||
SetOffset(glyphPositions, i, textScale, ref glyphOffsets); |
|||
shapedBuffer[i] = targetInfo; |
|||
} |
|||
|
|||
return new GlyphRun(glyphTypeface, fontRenderingEmSize, |
|||
new ReadOnlySlice<ushort>(glyphIndices), |
|||
new ReadOnlySlice<double>(glyphAdvances), |
|||
new ReadOnlySlice<Vector>(glyphOffsets), |
|||
text, |
|||
new ReadOnlySlice<ushort>(clusters), |
|||
buffer.Direction == Direction.LeftToRight ? 0 : 1); |
|||
return shapedBuffer; |
|||
} |
|||
} |
|||
|
|||
private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text) |
|||
private static void MergeBreakPair(Buffer buffer) |
|||
{ |
|||
buffer.ContentType = ContentType.Unicode; |
|||
var length = buffer.Length; |
|||
|
|||
var i = 0; |
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var second = glyphInfos[length - 1]; |
|||
|
|||
while (i < text.Length) |
|||
if (!new Codepoint((int)second.Codepoint).IsBreakChar) |
|||
{ |
|||
var codepoint = Codepoint.ReadAt(text, i, out var count); |
|||
return; |
|||
} |
|||
|
|||
var cluster = (uint)(text.Start + i); |
|||
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') |
|||
{ |
|||
var first = glyphInfos[length - 2]; |
|||
|
|||
first.Codepoint = '\u200C'; |
|||
second.Codepoint = '\u200C'; |
|||
second.Cluster = first.Cluster; |
|||
|
|||
if (codepoint.IsBreakChar) |
|||
unsafe |
|||
{ |
|||
if (i + 1 < text.Length) |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 2]) |
|||
{ |
|||
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); |
|||
|
|||
if (nextCodepoint == '\n' && codepoint == '\r') |
|||
{ |
|||
count++; |
|||
|
|||
buffer.Add('\u200C', cluster); |
|||
|
|||
buffer.Add('\u200D', cluster); |
|||
} |
|||
else |
|||
{ |
|||
buffer.Add('\u200C', cluster); |
|||
} |
|||
*p = first; |
|||
} |
|||
else |
|||
|
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
buffer.Add('\u200C', cluster); |
|||
*p = second; |
|||
} |
|||
} |
|||
else |
|||
} |
|||
else |
|||
{ |
|||
second.Codepoint = '\u200C'; |
|||
|
|||
unsafe |
|||
{ |
|||
buffer.Add(codepoint, cluster); |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
*p = second; |
|||
} |
|||
} |
|||
|
|||
i += count; |
|||
} |
|||
} |
|||
|
|||
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
|||
ref Vector[] offsetBuffer) |
|||
private static Vector GetGlyphOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
var position = glyphPositions[index]; |
|||
|
|||
if (position.XOffset == 0 && position.YOffset == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
offsetBuffer ??= new Vector[glyphPositions.Length]; |
|||
|
|||
var offsetX = position.XOffset * textScale; |
|||
|
|||
var offsetY = position.YOffset * textScale; |
|||
|
|||
offsetBuffer[index] = new Vector(offsetX, offsetY); |
|||
return new Vector(offsetX, offsetY); |
|||
} |
|||
|
|||
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
|||
ref double[] advanceBuffer) |
|||
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
advanceBuffer ??= new double[glyphPositions.Length]; |
|||
|
|||
// Depends on direction of layout
|
|||
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
|
|||
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; |
|||
// glyphPositions[index].YAdvance * textScale;
|
|||
return glyphPositions[index].XAdvance * textScale; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,129 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using DWrite = SharpDX.DirectWrite; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class FormattedTextImpl : IFormattedTextImpl |
|||
{ |
|||
public FormattedTextImpl( |
|||
string text, |
|||
Typeface typeface, |
|||
double fontSize, |
|||
TextAlignment textAlignment, |
|||
TextWrapping wrapping, |
|||
Size constraint, |
|||
IReadOnlyList<FormattedTextStyleSpan> spans) |
|||
{ |
|||
Text = text; |
|||
|
|||
var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; |
|||
var familyName = font.FontFamily.FamilyNames.GetString(0); |
|||
using (var textFormat = new DWrite.TextFormat( |
|||
Direct2D1Platform.DirectWriteFactory, |
|||
familyName, |
|||
font.FontFamily.FontCollection, |
|||
(DWrite.FontWeight)typeface.Weight, |
|||
(DWrite.FontStyle)typeface.Style, |
|||
DWrite.FontStretch.Normal, |
|||
(float)fontSize)) |
|||
{ |
|||
textFormat.WordWrapping = |
|||
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; |
|||
|
|||
TextLayout = new DWrite.TextLayout( |
|||
Direct2D1Platform.DirectWriteFactory, |
|||
Text ?? string.Empty, |
|||
textFormat, |
|||
(float)constraint.Width, |
|||
(float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() }; |
|||
} |
|||
|
|||
if (spans != null) |
|||
{ |
|||
foreach (var span in spans) |
|||
{ |
|||
ApplySpan(span); |
|||
} |
|||
} |
|||
|
|||
Bounds = Measure(); |
|||
} |
|||
|
|||
public Size Constraint => new Size(TextLayout.MaxWidth, TextLayout.MaxHeight); |
|||
|
|||
public Rect Bounds { get; } |
|||
|
|||
public string Text { get; } |
|||
|
|||
public DWrite.TextLayout TextLayout { get; } |
|||
|
|||
public IEnumerable<FormattedTextLine> GetLines() |
|||
{ |
|||
var result = TextLayout.GetLineMetrics(); |
|||
return from line in result select new FormattedTextLine(line.Length, line.Height); |
|||
} |
|||
|
|||
public TextHitTestResult HitTestPoint(Point point) |
|||
{ |
|||
var result = TextLayout.HitTestPoint( |
|||
(float)point.X, |
|||
(float)point.Y, |
|||
out var isTrailingHit, |
|||
out var isInside); |
|||
|
|||
return new TextHitTestResult |
|||
{ |
|||
IsInside = isInside, |
|||
TextPosition = result.TextPosition, |
|||
IsTrailing = isTrailingHit, |
|||
}; |
|||
} |
|||
|
|||
public Rect HitTestTextPosition(int index) |
|||
{ |
|||
var result = TextLayout.HitTestTextPosition(index, false, out _, out _); |
|||
|
|||
return new Rect(result.Left, result.Top, result.Width, result.Height); |
|||
} |
|||
|
|||
public IEnumerable<Rect> HitTestTextRange(int index, int length) |
|||
{ |
|||
var result = TextLayout.HitTestTextRange(index, length, 0, 0); |
|||
return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height)); |
|||
} |
|||
|
|||
private void ApplySpan(FormattedTextStyleSpan span) |
|||
{ |
|||
if (span.Length > 0) |
|||
{ |
|||
if (span.ForegroundBrush != null) |
|||
{ |
|||
TextLayout.SetDrawingEffect( |
|||
new BrushWrapper(span.ForegroundBrush.ToImmutable()), |
|||
new DWrite.TextRange(span.StartIndex, span.Length)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Rect Measure() |
|||
{ |
|||
var metrics = TextLayout.Metrics; |
|||
|
|||
var width = metrics.WidthIncludingTrailingWhitespace; |
|||
|
|||
if (float.IsNaN(width)) |
|||
{ |
|||
width = metrics.Width; |
|||
} |
|||
|
|||
return new Rect( |
|||
TextLayout.Metrics.Left, |
|||
TextLayout.Metrics.Top, |
|||
width, |
|||
TextLayout.Metrics.Height); |
|||
} |
|||
} |
|||
} |
|||
@ -1,145 +1,142 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
using HarfBuzzSharp; |
|||
using Buffer = HarfBuzzSharp.Buffer; |
|||
using GlyphInfo = HarfBuzzSharp.GlyphInfo; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class TextShaperImpl : ITextShaperImpl |
|||
|
|||
internal class TextShaperImpl : ITextShaperImpl |
|||
{ |
|||
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) |
|||
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, GlyphTypeface typeface, double fontRenderingEmSize, |
|||
CultureInfo culture, sbyte bidiLevel) |
|||
{ |
|||
using (var buffer = new Buffer()) |
|||
{ |
|||
FillBuffer(buffer, text); |
|||
|
|||
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); |
|||
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); |
|||
|
|||
MergeBreakPair(buffer); |
|||
|
|||
buffer.GuessSegmentProperties(); |
|||
|
|||
var glyphTypeface = typeface.GlyphTypeface; |
|||
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; |
|||
|
|||
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; |
|||
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); |
|||
|
|||
var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; |
|||
|
|||
font.Shape(buffer); |
|||
|
|||
if (buffer.Direction == Direction.RightToLeft) |
|||
{ |
|||
buffer.Reverse(); |
|||
} |
|||
|
|||
font.GetScale(out var scaleX, out _); |
|||
|
|||
var textScale = fontRenderingEmSize / scaleX; |
|||
|
|||
var bufferLength = buffer.Length; |
|||
|
|||
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); |
|||
|
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var glyphPositions = buffer.GetGlyphPositionSpan(); |
|||
|
|||
var glyphIndices = new ushort[bufferLength]; |
|||
|
|||
var clusters = new ushort[bufferLength]; |
|||
for (var i = 0; i < bufferLength; i++) |
|||
{ |
|||
var sourceInfo = glyphInfos[i]; |
|||
|
|||
double[] glyphAdvances = null; |
|||
var glyphIndex = (ushort)sourceInfo.Codepoint; |
|||
|
|||
Vector[] glyphOffsets = null; |
|||
var glyphCluster = (int)sourceInfo.Cluster; |
|||
|
|||
for (var i = 0; i < bufferLength; i++) |
|||
{ |
|||
glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; |
|||
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); |
|||
|
|||
clusters[i] = (ushort)glyphInfos[i].Cluster; |
|||
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); |
|||
|
|||
if (!glyphTypeface.IsFixedPitch) |
|||
{ |
|||
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); |
|||
} |
|||
var targetInfo = |
|||
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, |
|||
glyphOffset); |
|||
|
|||
SetOffset(glyphPositions, i, textScale, ref glyphOffsets); |
|||
shapedBuffer[i] = targetInfo; |
|||
} |
|||
|
|||
return new GlyphRun(glyphTypeface, fontRenderingEmSize, |
|||
new ReadOnlySlice<ushort>(glyphIndices), |
|||
new ReadOnlySlice<double>(glyphAdvances), |
|||
new ReadOnlySlice<Vector>(glyphOffsets), |
|||
text, |
|||
new ReadOnlySlice<ushort>(clusters)); |
|||
return shapedBuffer; |
|||
} |
|||
} |
|||
|
|||
private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text) |
|||
private static void MergeBreakPair(Buffer buffer) |
|||
{ |
|||
buffer.ContentType = ContentType.Unicode; |
|||
var length = buffer.Length; |
|||
|
|||
var i = 0; |
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var second = glyphInfos[length - 1]; |
|||
|
|||
while (i < text.Length) |
|||
if (!new Codepoint((int)second.Codepoint).IsBreakChar) |
|||
{ |
|||
var codepoint = Codepoint.ReadAt(text, i, out var count); |
|||
return; |
|||
} |
|||
|
|||
var cluster = (uint)(text.Start + i); |
|||
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') |
|||
{ |
|||
var first = glyphInfos[length - 2]; |
|||
|
|||
first.Codepoint = '\u200C'; |
|||
second.Codepoint = '\u200C'; |
|||
second.Cluster = first.Cluster; |
|||
|
|||
if (codepoint.IsBreakChar) |
|||
unsafe |
|||
{ |
|||
if (i + 1 < text.Length) |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 2]) |
|||
{ |
|||
var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); |
|||
|
|||
if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r') |
|||
{ |
|||
count++; |
|||
|
|||
buffer.Add('\u200C', cluster); |
|||
|
|||
buffer.Add('\u200D', cluster); |
|||
} |
|||
else |
|||
{ |
|||
buffer.Add('\u200C', cluster); |
|||
} |
|||
*p = first; |
|||
} |
|||
else |
|||
|
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
buffer.Add('\u200C', cluster); |
|||
*p = second; |
|||
} |
|||
} |
|||
else |
|||
} |
|||
else |
|||
{ |
|||
second.Codepoint = '\u200C'; |
|||
|
|||
unsafe |
|||
{ |
|||
buffer.Add(codepoint, cluster); |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
*p = second; |
|||
} |
|||
} |
|||
|
|||
i += count; |
|||
} |
|||
} |
|||
|
|||
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
|||
ref Vector[] offsetBuffer) |
|||
private static Vector GetGlyphOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
var position = glyphPositions[index]; |
|||
|
|||
if (position.XOffset == 0 && position.YOffset == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
offsetBuffer ??= new Vector[glyphPositions.Length]; |
|||
|
|||
var offsetX = position.XOffset * textScale; |
|||
|
|||
var offsetY = position.YOffset * textScale; |
|||
|
|||
offsetBuffer[index] = new Vector(offsetX, offsetY); |
|||
return new Vector(offsetX, offsetY); |
|||
} |
|||
|
|||
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale, |
|||
ref double[] advanceBuffer) |
|||
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
advanceBuffer ??= new double[glyphPositions.Length]; |
|||
|
|||
// Depends on direction of layout
|
|||
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
|
|||
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; |
|||
return glyphPositions[index].XAdvance * textScale; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,36 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Benchmarks |
|||
{ |
|||
internal class NullFormattedTextImpl : IFormattedTextImpl |
|||
{ |
|||
public Size Constraint { get; } |
|||
|
|||
public Rect Bounds { get; } |
|||
|
|||
public string Text { get; } |
|||
|
|||
public IEnumerable<FormattedTextLine> GetLines() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public TextHitTestResult HitTestPoint(Point point) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public Rect HitTestTextPosition(int index) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public IEnumerable<Rect> HitTestTextRange(int index, int length) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,267 +0,0 @@ |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Xunit; |
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests |
|||
#else
|
|||
|
|||
using Avalonia.Direct2D1.RenderTests; |
|||
|
|||
namespace Avalonia.Direct2D1.RenderTests.Media |
|||
#endif
|
|||
{ |
|||
public class FormattedTextImplTests : TestBase |
|||
{ |
|||
private const string FontName = "Courier New"; |
|||
private const double FontSize = 12; |
|||
private const double MediumFontSize = 18; |
|||
private const double BigFontSize = 32; |
|||
private const double FontSizeHeight = 13.594;//real value 13.59375
|
|||
private const string stringword = "word"; |
|||
private const string stringmiddle = "The quick brown fox jumps over the lazy dog"; |
|||
private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog"; |
|||
private const string stringmiddle3lines = "01234567\n\n0123456789"; |
|||
private const string stringmiddlenewlines = "012345678\r 1234567\r\n 12345678\n0123456789"; |
|||
|
|||
private const string stringlong = |
|||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis " + |
|||
"aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero" + |
|||
" at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus " + |
|||
"pretium ornare est."; |
|||
|
|||
public FormattedTextImplTests() |
|||
: base(@"Media\FormattedText") |
|||
{ |
|||
} |
|||
|
|||
private IFormattedTextImpl Create(string text, |
|||
string fontFamily, |
|||
double fontSize, |
|||
FontStyle fontStyle, |
|||
TextAlignment textAlignment, |
|||
FontWeight fontWeight, |
|||
TextWrapping wrapping, |
|||
double widthConstraint) |
|||
{ |
|||
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
|||
return r.CreateFormattedText(text, |
|||
new Typeface(fontFamily, fontStyle, fontWeight), |
|||
fontSize, |
|||
textAlignment, |
|||
wrapping, |
|||
widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), |
|||
null); |
|||
} |
|||
|
|||
private IFormattedTextImpl Create(string text, double fontSize) |
|||
{ |
|||
return Create(text, FontName, fontSize, |
|||
FontStyle.Normal, TextAlignment.Left, |
|||
FontWeight.Normal, TextWrapping.NoWrap, |
|||
-1); |
|||
} |
|||
|
|||
private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint) |
|||
{ |
|||
return Create(text, FontName, fontSize, |
|||
FontStyle.Normal, alignment, |
|||
FontWeight.Normal, TextWrapping.NoWrap, |
|||
widthConstraint); |
|||
} |
|||
|
|||
private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint) |
|||
{ |
|||
return Create(text, FontName, fontSize, |
|||
FontStyle.Normal, TextAlignment.Left, |
|||
FontWeight.Normal, wrap, |
|||
widthConstraint); |
|||
} |
|||
|
|||
|
|||
[Theory] |
|||
[InlineData("", FontSize, 0, FontSizeHeight)] |
|||
[InlineData("x", FontSize, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, FontSize, 28.80, FontSizeHeight)] |
|||
[InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)] |
|||
[InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)] |
|||
[InlineData(stringmiddle, BigFontSize, 825.73, 36.25)] |
|||
[InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)] |
|||
[InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)] |
|||
[InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)] |
|||
[InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)] |
|||
[InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] |
|||
public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) |
|||
{ |
|||
var fmt = Create(input, fontSize); |
|||
var size = fmt.Bounds.Size; |
|||
|
|||
Assert.Equal(expWidth, size.Width, 2); |
|||
Assert.Equal(expHeight, size.Height, 2); |
|||
|
|||
var linesHeight = fmt.GetLines().Sum(l => l.Height); |
|||
|
|||
Assert.Equal(expHeight, linesHeight, 2); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("", 1, -1, TextWrapping.NoWrap)] |
|||
[InlineData("x", 1, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringword, 1, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringmiddle, 1, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringmiddle, 3, 150, TextWrapping.Wrap)] |
|||
[InlineData(stringmiddle2lines, 2, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringmiddle2lines, 3, 150, TextWrapping.Wrap)] |
|||
[InlineData(stringlong, 1, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringlong, 18, 150, TextWrapping.Wrap)] |
|||
[InlineData(stringmiddlenewlines, 4, -1, TextWrapping.NoWrap)] |
|||
[InlineData(stringmiddlenewlines, 4, 150, TextWrapping.Wrap)] |
|||
public void Should_Break_Lines_String_Correctly(string input, |
|||
int linesCount, |
|||
double widthConstraint, |
|||
TextWrapping wrap) |
|||
{ |
|||
var fmt = Create(input, FontSize, wrap, widthConstraint); |
|||
var constrained = fmt; |
|||
|
|||
var lines = constrained.GetLines().ToArray(); |
|||
Assert.Equal(linesCount, lines.Count()); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("x", 0, 0, true, false, 0)] |
|||
[InlineData(stringword, -1, -1, false, false, 0)] |
|||
[InlineData(stringword, 25, 13, true, false, 3)] |
|||
[InlineData(stringword, 28.70, 13.5, true, true, 3)] |
|||
[InlineData(stringword, 30, 13, false, true, 3)] |
|||
[InlineData(stringword + "\r\n", 30, 13, false, false, 4)] |
|||
[InlineData(stringword + "\r\nnext", 30, 13, false, false, 4)] |
|||
[InlineData(stringword, 300, 13, false, true, 3)] |
|||
[InlineData(stringword + "\r\n", 300, 13, false, false, 4)] |
|||
[InlineData(stringword + "\r\nnext", 300, 13, false, false, 4)] |
|||
[InlineData(stringword, 300, 300, false, true, 3)] |
|||
//TODO: Direct2D implementation return textposition 6
|
|||
//but the text is 6 length, can't find the logic for me it should be 5
|
|||
//[InlineData(stringword + "\r\n", 300, 300, false, false, 6)]
|
|||
[InlineData(stringword + "\r\nnext", 300, 300, false, true, 9)] |
|||
[InlineData(stringword + "\r\nnext", 300, 25, false, true, 9)] |
|||
[InlineData(stringword, 28, 15, false, true, 3)] |
|||
[InlineData(stringword, 30, 15, false, true, 3)] |
|||
[InlineData(stringmiddle3lines, 30, 15, false, false, 9)] |
|||
[InlineData(stringmiddle3lines, 500, 13, false, false, 8)] |
|||
[InlineData(stringmiddle3lines, 30, 25, false, false, 9)] |
|||
[InlineData(stringmiddle3lines, -1, 30, false, false, 10)] |
|||
public void Should_HitTestPoint_Correctly(string input, |
|||
double x, double y, |
|||
bool isInside, bool isTrailing, int pos) |
|||
{ |
|||
var fmt = Create(input, FontSize); |
|||
var htRes = fmt.HitTestPoint(new Point(x, y)); |
|||
|
|||
Assert.Equal(pos, htRes.TextPosition); |
|||
Assert.Equal(isInside, htRes.IsInside); |
|||
Assert.Equal(isTrailing, htRes.IsTrailing); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("", 0, 0, 0, 0, FontSizeHeight)] |
|||
[InlineData("x", 0, 0, 0, 7.20, FontSizeHeight)] |
|||
[InlineData("x", -1, 7.20, 0, 0, FontSizeHeight)] |
|||
[InlineData(stringword, 3, 21.60, 0, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, 4, 21.60 + 7.20, 0, 0, FontSizeHeight)] |
|||
[InlineData(stringmiddlenewlines, 10, 0, FontSizeHeight, 7.20, FontSizeHeight)] |
|||
[InlineData(stringmiddlenewlines, 15, 36.01, FontSizeHeight, 7.20, FontSizeHeight)] |
|||
[InlineData(stringmiddlenewlines, 20, 0, 2 * FontSizeHeight, 7.20, FontSizeHeight)] |
|||
[InlineData(stringmiddlenewlines, -1, 72.01, 3 * FontSizeHeight, 0, FontSizeHeight)] |
|||
public void Should_HitTestPosition_Correctly(string input, |
|||
int index, double x, double y, double width, double height) |
|||
{ |
|||
var fmt = Create(input, FontSize); |
|||
var r = fmt.HitTestTextPosition(index); |
|||
|
|||
Assert.Equal(x, r.X, 2); |
|||
Assert.Equal(y, r.Y, 2); |
|||
Assert.Equal(width, r.Width, 2); |
|||
Assert.Equal(height, r.Height, 2); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] |
|||
public void Should_HitTestPosition_RigthAlign_Correctly( |
|||
string input, int index, double widthConstraint, |
|||
double x, double y, double width, double height) |
|||
{ |
|||
//parse expected
|
|||
var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint); |
|||
var constrained = fmt; |
|||
var r = constrained.HitTestTextPosition(index); |
|||
|
|||
Assert.Equal(x, r.X, 2); |
|||
Assert.Equal(y, r.Y, 2); |
|||
Assert.Equal(width, r.Width, 2); |
|||
Assert.Equal(height, r.Height, 2); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("x", 0, 200, 100 - 7.20 / 2, 0, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, 0, 200, 85.6, 0, 7.20, FontSizeHeight)] |
|||
[InlineData(stringword, 3, 200, 100 + 7.20, 0, 7.20, FontSizeHeight)] |
|||
public void Should_HitTestPosition_CenterAlign_Correctly( |
|||
string input, int index, double widthConstraint, |
|||
double x, double y, double width, double height) |
|||
{ |
|||
//parse expected
|
|||
var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint); |
|||
var constrained = fmt; |
|||
var r = constrained.HitTestTextPosition(index); |
|||
|
|||
Assert.Equal(x, r.X, 2); |
|||
Assert.Equal(y, r.Y, 2); |
|||
Assert.Equal(width, r.Width, 2); |
|||
Assert.Equal(height, r.Height, 2); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("x", 0, 1, "0,0,7.20,13.59")] |
|||
[InlineData(stringword, 0, 4, "0,0,28.80,13.59")] |
|||
[InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")] |
|||
[InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")] |
|||
[InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")] |
|||
[InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")] |
|||
public void Should_HitTestRange_Correctly(string input, |
|||
int index, int length, |
|||
string expectedRects) |
|||
{ |
|||
//parse expected result
|
|||
var rects = expectedRects.Split(';').Select(s => |
|||
{ |
|||
double[] v = s.Split(',') |
|||
.Select(sd => double.Parse(sd, CultureInfo.InvariantCulture)).ToArray(); |
|||
return new Rect(v[0], v[1], v[2], v[3]); |
|||
}).ToArray(); |
|||
|
|||
var fmt = Create(input, FontSize); |
|||
var htRes = fmt.HitTestTextRange(index, length).ToArray(); |
|||
|
|||
Assert.Equal(rects.Length, htRes.Length); |
|||
|
|||
for (int i = 0; i < rects.Length; i++) |
|||
{ |
|||
var exr = rects[i]; |
|||
var r = htRes[i]; |
|||
|
|||
Assert.Equal(exr.X, r.X, 2); |
|||
Assert.Equal(exr.Y, r.Y, 2); |
|||
Assert.Equal(exr.Width, r.Width, 2); |
|||
Assert.Equal(exr.Height, r.Height, 2); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting |
|||
{ |
|||
public class BiDiAlgorithmTests |
|||
{ |
|||
private readonly ITestOutputHelper _outputHelper; |
|||
|
|||
public BiDiAlgorithmTests(ITestOutputHelper outputHelper) |
|||
{ |
|||
_outputHelper = outputHelper; |
|||
} |
|||
|
|||
[Fact(Skip = "Only run when the Unicode spec changes.")] |
|||
public void Should_Process() |
|||
{ |
|||
var generator = new BiDiTestDataGenerator(); |
|||
|
|||
foreach(var testData in generator) |
|||
{ |
|||
Assert.True(Run(testData)); |
|||
} |
|||
} |
|||
|
|||
private bool Run(BiDiTestData testData) |
|||
{ |
|||
var bidi = BidiAlgorithm.Instance.Value; |
|||
|
|||
// Run the algorithm...
|
|||
ArraySlice<sbyte> resultLevels; |
|||
|
|||
bidi.Process( |
|||
testData.Classes, |
|||
ArraySlice<BidiPairedBracketType>.Empty, |
|||
ArraySlice<int>.Empty, |
|||
testData.ParagraphEmbeddingLevel, |
|||
false, |
|||
null, |
|||
null, |
|||
null); |
|||
|
|||
resultLevels = bidi.ResolvedLevels; |
|||
|
|||
// Check the results match
|
|||
var pass = true; |
|||
|
|||
if (resultLevels.Length == testData.Levels.Length) |
|||
{ |
|||
for (var i = 0; i < testData.Levels.Length; i++) |
|||
{ |
|||
if (testData.Levels[i] == -1) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (resultLevels[i] != testData.Levels[i]) |
|||
{ |
|||
pass = false; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
pass = false; |
|||
} |
|||
|
|||
if (!pass) |
|||
{ |
|||
_outputHelper.WriteLine($"Failed line {testData.LineNumber}"); |
|||
_outputHelper.WriteLine($" Data: {string.Join(" ", testData.Classes)}"); |
|||
_outputHelper.WriteLine($" Embed Level: {testData.ParagraphEmbeddingLevel}"); |
|||
_outputHelper.WriteLine($" Expected: {string.Join(" ", testData.Levels)}"); |
|||
_outputHelper.WriteLine($" Actual: {string.Join(" ", resultLevels)}"); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting |
|||
{ |
|||
internal class BiDiClassTestDataGenerator : IEnumerable<BiDiClassData> |
|||
{ |
|||
private readonly List<BiDiClassData> _testData; |
|||
|
|||
public BiDiClassTestDataGenerator() |
|||
{ |
|||
_testData = ReadData(); |
|||
} |
|||
|
|||
public IEnumerator<BiDiClassData> GetEnumerator() |
|||
{ |
|||
return _testData.GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
private static List<BiDiClassData> ReadData() |
|||
{ |
|||
var testData = new List<BiDiClassData>(); |
|||
|
|||
using (var client = new HttpClient()) |
|||
{ |
|||
var url = Path.Combine(UnicodeDataGenerator.Ucd, "BidiCharacterTest.txt"); |
|||
|
|||
using (var result = client.GetAsync(url).GetAwaiter().GetResult()) |
|||
{ |
|||
if (!result.IsSuccessStatusCode) |
|||
return testData; |
|||
|
|||
using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) |
|||
using (var reader = new StreamReader(stream)) |
|||
{ |
|||
var lineNumber = 0; |
|||
|
|||
// Process each line
|
|||
while (!reader.EndOfStream) |
|||
{ |
|||
var line = reader.ReadLine(); |
|||
|
|||
lineNumber++; |
|||
|
|||
if (line == null) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (line.StartsWith("#") || string.IsNullOrEmpty(line)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Split into fields
|
|||
var fields = line.Split(';'); |
|||
|
|||
// Parse field 0 - code points
|
|||
var codePoints = fields[0].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => Convert.ToInt32(x, 16)).ToArray(); |
|||
|
|||
// Parse field 1 - paragraph level
|
|||
var paragraphLevel = sbyte.Parse(fields[1]); |
|||
|
|||
// Parse field 2 - resolved paragraph level
|
|||
var resolvedParagraphLevel = sbyte.Parse(fields[2]); |
|||
|
|||
// Parse field 3 - resolved levels
|
|||
var resolvedLevels = fields[3].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => x == "x" ? (sbyte)-1 : Convert.ToSByte(x)).ToArray(); |
|||
|
|||
// Parse field 4 - resolved levels
|
|||
var resolvedOrder = fields[4].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => Convert.ToInt32(x)).ToArray(); |
|||
|
|||
testData.Add(new BiDiClassData |
|||
{ |
|||
LineNumber = lineNumber, |
|||
CodePoints = codePoints, |
|||
ParagraphLevel = paragraphLevel, |
|||
ResolvedParagraphLevel = resolvedParagraphLevel, |
|||
ResolvedLevels = resolvedLevels, |
|||
ResolvedOrder = resolvedOrder |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return testData; |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
internal struct BiDiClassData |
|||
{ |
|||
public int LineNumber { get; set; } |
|||
public int[] CodePoints{ get; set; } |
|||
public sbyte ParagraphLevel{ get; set; } |
|||
public sbyte ResolvedParagraphLevel{ get; set; } |
|||
public sbyte[] ResolvedLevels{ get; set; } |
|||
public int[] ResolvedOrder{ get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using System.Text; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting |
|||
{ |
|||
public class BiDiClassTests |
|||
{ |
|||
private readonly ITestOutputHelper _outputHelper; |
|||
|
|||
public BiDiClassTests(ITestOutputHelper outputHelper) |
|||
{ |
|||
_outputHelper = outputHelper; |
|||
} |
|||
|
|||
[Fact(Skip = "Only run when the Unicode spec changes.")] |
|||
public void Should_Resolve() |
|||
{ |
|||
var generator = new BiDiClassTestDataGenerator(); |
|||
|
|||
foreach (var testData in generator) |
|||
{ |
|||
Assert.True(Run(testData)); |
|||
} |
|||
} |
|||
|
|||
private bool Run(BiDiClassData t) |
|||
{ |
|||
var bidi = BidiAlgorithm.Instance.Value; |
|||
var bidiData = new BidiData(t.ParagraphLevel); |
|||
|
|||
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray()); |
|||
|
|||
// Append
|
|||
bidiData.Append(text.AsMemory()); |
|||
|
|||
// Act
|
|||
bidi.Process(bidiData); |
|||
|
|||
var resultLevels = bidi.ResolvedLevels; |
|||
var resultParagraphLevel = bidi.ResolvedParagraphEmbeddingLevel; |
|||
|
|||
// Assert
|
|||
var passed = true; |
|||
|
|||
if (t.ResolvedParagraphLevel != resultParagraphLevel) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
for (var i = 0; i < t.ResolvedLevels.Length; i++) |
|||
{ |
|||
if (t.ResolvedLevels[i] == -1) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (t.ResolvedLevels[i] != resultLevels[i]) |
|||
{ |
|||
passed = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (passed) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
_outputHelper.WriteLine($"Failed line {t.LineNumber}"); |
|||
|
|||
_outputHelper.WriteLine( |
|||
$" Code Points: {string.Join(" ", t.CodePoints.Select(x => x.ToString("X4")))}"); |
|||
|
|||
_outputHelper.WriteLine( |
|||
$" Pair Bracket Types: {string.Join(" ", bidiData.PairedBracketTypes.Select(x => " " + x.ToString()))}"); |
|||
|
|||
_outputHelper.WriteLine( |
|||
$" Pair Bracket Values: {string.Join(" ", bidiData.PairedBracketValues.Select(x => x.ToString("X4")))}"); |
|||
_outputHelper.WriteLine($" Embed Level: {t.ParagraphLevel}"); |
|||
_outputHelper.WriteLine($" Expected Embed Level: {t.ResolvedParagraphLevel}"); |
|||
_outputHelper.WriteLine($" Actual Embed Level: {resultParagraphLevel}"); |
|||
_outputHelper.WriteLine($" Directionality: {string.Join(" ", bidiData.Classes)}"); |
|||
_outputHelper.WriteLine($" Expected Levels: {string.Join(" ", t.ResolvedLevels)}"); |
|||
_outputHelper.WriteLine($" Actual Levels: {string.Join(" ", resultLevels)}"); |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting |
|||
{ |
|||
public class BiDiPairedBracketTypeTests |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting |
|||
{ |
|||
internal class BiDiTestDataGenerator : IEnumerable<BiDiTestData> |
|||
{ |
|||
private readonly List<BiDiTestData> _testData; |
|||
|
|||
public BiDiTestDataGenerator() |
|||
{ |
|||
_testData = ReadTestData(); |
|||
} |
|||
|
|||
public IEnumerator<BiDiTestData> GetEnumerator() |
|||
{ |
|||
return _testData.GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
private static List<BiDiTestData> ReadTestData() |
|||
{ |
|||
var testData = new List<BiDiTestData>(); |
|||
|
|||
using (var client = new HttpClient()) |
|||
{ |
|||
var url = Path.Combine(UnicodeDataGenerator.Ucd, "BidiTest.txt"); |
|||
|
|||
using (var result = client.GetAsync(url).GetAwaiter().GetResult()) |
|||
{ |
|||
if (!result.IsSuccessStatusCode) |
|||
return testData; |
|||
|
|||
using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) |
|||
using (var reader = new StreamReader(stream)) |
|||
{ |
|||
var lineNumber = 0; |
|||
|
|||
// Process each line
|
|||
int[] levels = null; |
|||
|
|||
while (!reader.EndOfStream) |
|||
{ |
|||
var line = reader.ReadLine(); |
|||
|
|||
lineNumber++; |
|||
|
|||
if (line == null) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (line.StartsWith("#") || string.IsNullOrEmpty(line)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Directive?
|
|||
if (line.StartsWith("@")) |
|||
{ |
|||
if (line.StartsWith("@Levels:")) |
|||
{ |
|||
levels = line.Substring(8).Trim().Split(' ').Where(x => x.Length > 0).Select(x => |
|||
{ |
|||
if (x == "x") |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
return int.Parse(x); |
|||
|
|||
}).ToArray(); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
|
|||
// Split data line
|
|||
var parts = line.Split(';'); |
|||
|
|||
// Get the directions
|
|||
var directions = parts[0].Split(' ').Select(PropertyValueAliasHelper.GetBiDiClass) |
|||
.ToArray(); |
|||
|
|||
// Get the bit set
|
|||
var bitset = Convert.ToInt32(parts[1].Trim(), 16); |
|||
|
|||
for (var bit = 1; bit < 8; bit <<= 1) |
|||
{ |
|||
if ((bitset & bit) == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
sbyte paragraphEmbeddingLevel; |
|||
|
|||
switch (bit) |
|||
{ |
|||
case 1: |
|||
paragraphEmbeddingLevel = 2; // Auto
|
|||
break; |
|||
|
|||
case 2: |
|||
paragraphEmbeddingLevel = 0; // LTR
|
|||
break; |
|||
|
|||
case 4: |
|||
paragraphEmbeddingLevel = 1; // RTL
|
|||
break; |
|||
|
|||
default: |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
testData.Add(new BiDiTestData |
|||
{ |
|||
LineNumber = lineNumber, |
|||
Classes = directions, |
|||
ParagraphEmbeddingLevel = paragraphEmbeddingLevel, |
|||
Levels = levels |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return testData; |
|||
} |
|||
} |
|||
|
|||
internal class BiDiTestData |
|||
{ |
|||
public int LineNumber { get; set; } |
|||
public BidiClass[] Classes { get; set; } |
|||
public sbyte ParagraphEmbeddingLevel { get; set; } |
|||
public int[] Levels { get; set; } |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue