diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 774580415a..0bab473442 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1610,12 +1610,9 @@ namespace Avalonia.Media var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); + var text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length); TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - - var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length, - properties); - - return textCharacters; + return new TextCharacters(text, properties); } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 811479fde8..fc4bc6aa1c 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private IReadOnlyList _characters; + private ReadOnlyMemory _characters; private IReadOnlyList _glyphIndices; private IReadOnlyList? _glyphAdvances; private IReadOnlyList? _glyphOffsets; @@ -41,7 +41,7 @@ namespace Avalonia.Media public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList characters, + ReadOnlyMemory characters, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances = null, IReadOnlyList? glyphOffsets = null, @@ -141,7 +141,7 @@ namespace Avalonia.Media /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// - public IReadOnlyList Characters + public ReadOnlyMemory Characters { get => _characters; set => Set(ref _characters, value); @@ -600,9 +600,9 @@ namespace Avalonia.Media } } - if (Characters != null) + if (!Characters.IsEmpty) { - clusterLength = Characters.Count - characterLength; + clusterLength = Characters.Length - characterLength; } else { @@ -653,10 +653,10 @@ namespace Avalonia.Media } else { - if (Characters != null && Characters.Count > 0) + if (!Characters.IsEmpty) { firstCluster = 0; - lastCluster = Characters.Count - 1; + lastCluster = Characters.Length - 1; } } @@ -716,14 +716,15 @@ namespace Avalonia.Media glyphCount = 0; newLineLength = 0; var trailingWhitespaceLength = 0; + var charactersSpan = _characters.Span; - if (Characters != null) + if (!charactersSpan.IsEmpty) { if (GlyphClusters == null) { - for (var i = _characters.Count - 1; i >= 0;) + for (var i = charactersSpan.Length - 1; i >= 0;) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); + var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); if (!codepoint.IsWhiteSpace) { @@ -743,55 +744,52 @@ namespace Avalonia.Media } else { - if (Characters.Count > 0) + var characterIndex = charactersSpan.Length - 1; + + for (var i = GlyphClusters.Count - 1; i >= 0; i--) { - var characterIndex = Characters.Count - 1; + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } } @@ -804,14 +802,15 @@ namespace Avalonia.Media glyphCount = 0; newLineLength = 0; var trailingWhitespaceLength = 0; + var charactersSpan = Characters.Span; - if (Characters != null) + if (!charactersSpan.IsEmpty) { if (GlyphClusters == null) { - for (var i = 0; i < Characters.Count;) + for (var i = 0; i < charactersSpan.Length;) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); + var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); if (!codepoint.IsWhiteSpace) { @@ -836,7 +835,7 @@ namespace Avalonia.Media for (var i = 0; i < GlyphClusters.Count; i++) { var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); characterIndex += characterLength; diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs deleted file mode 100644 index 499026e8b3..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting -{ - public readonly struct CharacterBufferRange : IReadOnlyList - { - /// - /// Getting an empty character string - /// - public static CharacterBufferRange Empty => new CharacterBufferRange(); - - /// - /// Construct from character array - /// - /// character array - /// character buffer offset to the first character - /// character length - public CharacterBufferRange( - char[] characterArray, - int offsetToFirstChar, - int characterLength - ) - : this( - new CharacterBufferReference(characterArray, offsetToFirstChar), - characterLength - ) - { } - - /// - /// Construct from string - /// - /// character string - /// character buffer offset to the first character - /// character length - public CharacterBufferRange( - string characterString, - int offsetToFirstChar, - int characterLength - ) - : this( - new CharacterBufferReference(characterString, offsetToFirstChar), - characterLength - ) - { } - - /// - /// Construct a from - /// - /// character buffer reference - /// number of characters - public CharacterBufferRange( - CharacterBufferReference characterBufferReference, - int characterLength - ) - { - if (characterLength < 0) - { - throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); - } - - int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? - characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : - 0; - - if (characterLength > maxLength) - { - throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); - } - - CharacterBufferReference = characterBufferReference; - Length = characterLength; - } - - /// - /// Construct a from part of another - /// - internal CharacterBufferRange( - CharacterBufferRange characterBufferRange, - int offsetToFirstChar, - int characterLength - ) : - this( - characterBufferRange.CharacterBuffer, - characterBufferRange.OffsetToFirstChar + offsetToFirstChar, - characterLength - ) - { } - - - /// - /// Construct a from string - /// - internal CharacterBufferRange( - string charString - ) : - this( - charString, - 0, - charString.Length - ) - { } - - - /// - /// Construct from memory buffer - /// - internal CharacterBufferRange( - ReadOnlyMemory charBuffer, - int offsetToFirstChar, - int characterLength - ) : - this( - new CharacterBufferReference(charBuffer, offsetToFirstChar), - characterLength - ) - { } - - - /// - /// Construct a by extracting text info from a text run - /// - internal CharacterBufferRange(TextRun textRun) - { - CharacterBufferReference = textRun.CharacterBufferReference; - Length = textRun.Length; - } - - public char this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } -#endif - return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; - } - } - - /// - /// Gets a reference to the character buffer - /// - public CharacterBufferReference CharacterBufferReference { get; } - - /// - /// Gets the number of characters in text source character store - /// - public int Length { get; } - - /// - /// Gets a span from the character buffer range - /// - public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); - - /// - /// Gets the character memory buffer - /// - internal ReadOnlyMemory CharacterBuffer => CharacterBufferReference.CharacterBuffer; - - /// - /// Gets the character offset relative to the beginning of buffer to - /// the first character of the run - /// - internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar; - - /// - /// Indicate whether the character buffer range is empty - /// - internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; - - internal CharacterBufferRange Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new CharacterBufferRange(CharacterBufferReference, length); - } - - internal CharacterBufferRange Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - if (length == Length) - { - return new CharacterBufferRange(new CharacterBufferReference(), 0); - } - - var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); - - return new CharacterBufferRange(characterBufferReference, Length - length); - } - - /// - /// Compute hash code - /// - public override int GetHashCode() - { - return CharacterBufferReference.GetHashCode() ^ Length; - } - - /// - /// Test equality with the input object - /// - /// The object to test - public override bool Equals(object? obj) - { - if (obj is CharacterBufferRange range) - { - return Equals(range); - } - - return false; - } - - /// - /// Test equality with the input CharacterBufferRange - /// - /// The CharacterBufferRange value to test - public bool Equals(CharacterBufferRange value) - { - return CharacterBufferReference.Equals(value.CharacterBufferReference) - && Length == value.Length; - } - - /// - /// Compare two CharacterBufferRange for equality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) - { - return left.Equals(right); - } - - /// - /// Compare two CharacterBufferRange for inequality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) - { - return !(left == right); - } - - int IReadOnlyCollection.Count => Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs deleted file mode 100644 index 672fcf3377..0000000000 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Text character buffer reference - /// - public readonly struct CharacterBufferReference : IEquatable - { - /// - /// Construct character buffer reference from character array - /// - /// character array - /// character buffer offset to the first character - public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) - : this(characterArray.AsMemory(), offsetToFirstChar) - { } - - /// - /// Construct character buffer reference from string - /// - /// character string - /// character buffer offset to the first character - public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) - : this(characterString.AsMemory(), offsetToFirstChar) - { } - - /// - /// Construct character buffer reference from memory buffer - /// - internal CharacterBufferReference(ReadOnlyMemory characterBuffer, int offsetToFirstChar = 0) - { - if (offsetToFirstChar < 0) - { - throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); - } - - // maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset - // even in the case of an empty or null character buffer - var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); - if (offsetToFirstChar > maxOffset) - { - throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); - } - - CharacterBuffer = characterBuffer; - OffsetToFirstChar = offsetToFirstChar; - } - - /// - /// Gets the character memory buffer - /// - public ReadOnlyMemory CharacterBuffer { get; } - - /// - /// Gets the character offset relative to the beginning of buffer to - /// the first character of the run - /// - public int OffsetToFirstChar { get; } - - /// - /// Compute hash code - /// - public override int GetHashCode() - { - return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); - } - - /// - /// Test equality with the input object - /// - /// The object to test. - public override bool Equals(object? obj) - { - if (obj is CharacterBufferReference reference) - { - return Equals(reference); - } - - return false; - } - - /// - /// Test equality with the input CharacterBufferReference - /// - /// The characterBufferReference value to test - public bool Equals(CharacterBufferReference value) - { - return CharacterBuffer.Equals(value.CharacterBuffer); - } - - /// - /// Compare two CharacterBufferReference for equality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) - { - return left.Equals(right); - } - - /// - /// Compare two CharacterBufferReference for inequality - /// - /// left operand - /// right operand - /// whether or not two operands are equal - public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) - { - return !(left == right); - } - } -} - diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 49d94b511d..5c28989c7d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting { internal readonly struct FormattedTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; public FormattedTextSource(string text, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { - _text = new CharacterBufferRange(text); + _text = text; _defaultProperties = defaultProperties; _textModifier = textModifier; } @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting return null; } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsSpan(textSourceIndex); if (runText.IsEmpty) { @@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value); + return new TextCharacters(_text.AsMemory(textSourceIndex, textStyleRun.Length), textStyleRun.Value); } /// @@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting /// /// The created text style run. /// - private static ValueSpan CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, + private static ValueSpan CreateTextStyleRun(ReadOnlySpan text, int firstTextSourceIndex, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { if (textModifier == null || textModifier.Count == 0) @@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting return new ValueSpan(firstTextSourceIndex, length, currentProperties); } - private static int CoerceLength(CharacterBufferRange text, int length) + private static int CoerceLength(ReadOnlySpan text, int length) { var finalLength = 0; @@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting { var grapheme = graphemeEnumerator.Current; - finalLength += grapheme.Length; + finalLength += grapheme.Text.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 21e8ce089a..b518d47a6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -50,14 +50,14 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in lineImpl.TextRuns) { - var text = new CharacterBufferRange(textRun); + var text = textRun.Text; if (text.IsEmpty) { continue; } - var lineBreakEnumerator = new LineBreakEnumerator(text); + var lineBreakEnumerator = new LineBreakEnumerator(text.Span); while (lineBreakEnumerator.MoveNext()) { @@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.CharacterBufferReference.CharacterBuffer; + var text = textRun.Text; if (text.IsEmpty) { diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index af896b426d..b05fab08fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -9,21 +9,21 @@ namespace Avalonia.Media.TextFormatting { private static readonly IComparer s_clusterComparer = new CompareClusters(); private bool _bufferRented; - - public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : + this(text, + new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), + glyphTypeface, + fontRenderingEmSize, bidiLevel) { _bufferRented = true; Length = bufferLength; } - internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - CharacterBufferRange = characterBufferRange; + Text = text; GlyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); - public CharacterBufferRange CharacterBufferRange { get; } + public ReadOnlyMemory Text { get; } /// /// Finds a glyph index for given character index. @@ -113,7 +113,7 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - if (CharacterBufferRange.Length == length) + if (Text.Length == length) { return new SplitResult(this, null); } @@ -125,10 +125,10 @@ namespace Avalonia.Media.TextFormatting var glyphCount = FindGlyphIndex(start + length); - var first = new ShapedBuffer(CharacterBufferRange.Take(length), + var first = new ShapedBuffer(Text.Slice(0, length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); - var second = new ShapedBuffer(CharacterBufferRange.Skip(length), + var second = new ShapedBuffer(Text.Slice(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 665723b284..583f2e49f1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -13,8 +13,6 @@ namespace Avalonia.Media.TextFormatting public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties) { ShapedBuffer = shapedBuffer; - CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; - Length = shapedBuffer.CharacterBufferRange.Length; Properties = properties; TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); } @@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting public ShapedBuffer ShapedBuffer { get; } /// - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text + => ShapedBuffer.Text; /// public override TextRunProperties Properties { get; } /// - public override int Length { get; } + public override int Length + => ShapedBuffer.Text.Length; public TextMetrics TextMetrics { get; } @@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting { length = 0; var currentWidth = 0.0; + var charactersSpan = GlyphRun.Characters.Span; for (var i = 0; i < ShapedBuffer.Length; i++) { @@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting break; } - Codepoint.ReadAt(GlyphRun.Characters, length, out var count); + Codepoint.ReadAt(charactersSpan, length, out var count); length += count; currentWidth += advance; @@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting { length = 0; width = 0; + var charactersSpan = GlyphRun.Characters.Span; for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { @@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting break; } - Codepoint.ReadAt(GlyphRun.Characters, length, out var count); + Codepoint.ReadAt(charactersSpan, length, out var count); length += count; width += advance; @@ -192,7 +194,7 @@ namespace Avalonia.Media.TextFormatting return new GlyphRun( ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, - new CharacterBufferRange(CharacterBufferReference, Length), + Text, ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphOffsets, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 1a48151834..2525f0dbf9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -10,82 +10,34 @@ namespace Avalonia.Media.TextFormatting public class TextCharacters : TextRun { /// - /// Construct a run of text content from character array + /// Constructs a run for text content from a string. /// - public TextCharacters( - char[] characterArray, - int offsetToFirstChar, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(characterArray, offsetToFirstChar), - length, - textRunProperties - ) - { } - - - /// - /// Construct a run for text content from string - /// - public TextCharacters( - string characterString, - TextRunProperties textRunProperties - ) : - this( - characterString, - 0, // offsetToFirstChar - (characterString == null) ? 0 : characterString.Length, - textRunProperties - ) - { } - - /// - /// Construct a run for text content from string - /// - public TextCharacters( - string characterString, - int offsetToFirstChar, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(characterString, offsetToFirstChar), - length, - textRunProperties - ) - { } + public TextCharacters(string text, TextRunProperties textRunProperties) + : this(text.AsMemory(), textRunProperties) + { + } /// - /// Internal constructor of TextContent + /// Constructs a run for text content from a memory region. /// - public TextCharacters( - CharacterBufferReference characterBufferReference, - int length, - TextRunProperties textRunProperties - ) + public TextCharacters(ReadOnlyMemory text, TextRunProperties textRunProperties) { - if (length <= 0) - { - throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero"); - } - if (textRunProperties.FontRenderingEmSize <= 0) { - throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero"); + throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize, + $"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}"); } - CharacterBufferReference = characterBufferReference; - Length = length; + Text = text; Properties = textRunProperties; } /// - public override int Length { get; } + public override int Length + => Text.Length; /// - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text { get; } /// public override TextRunProperties Properties { get; } @@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); + var properties = Properties; - while (characterBufferRange.Length > 0) + while (!text.IsEmpty) { - var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); shapeableCharacters.Add(shapeableRun); - characterBufferRange = characterBufferRange.Skip(shapeableRun.Length); + text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } @@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting /// /// Creates a shapeable text run with unique properties. /// - /// The character buffer range to create text runs from. + /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. /// /// A list of shapeable text runs. - private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange, + private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; var previousTypeface = previousProperties?.Typeface; + var textSpan = text.Span; - if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) { if (script == Script.Common && previousTypeface is not null) { - if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) { - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount, + return new UnshapedTextRun(text.Slice(0, fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } if (previousTypeface is not null) { - if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) { - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } var codepoint = Codepoint.ReplacementCodepoint; - var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count)); + var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); while (codepointEnumerator.MoveNext()) { @@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(characterBufferRange); + var enumerator = new GraphemeEnumerator(textSpan); while (enumerator.MoveNext()) { @@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting break; } - count += grapheme.Length; + count += grapheme.Text.Length; } - return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); + return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); } /// /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// The character buffer range to shape. + /// The characters to shape. /// The typeface that is used to find matching characters. /// /// The shapeable length. /// /// internal static bool TryGetShapeableLength( - CharacterBufferRange characterBufferRange, + ReadOnlySpan text, Typeface typeface, Typeface? defaultTypeface, out int length, @@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting length = 0; script = Script.Unknown; - if (characterBufferRange.Length == 0) + if (text.IsEmpty) { return false; } @@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(characterBufferRange); + var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { @@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Length; + length += currentGrapheme.Text.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 9c201bda22..528cd45581 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -45,9 +45,7 @@ namespace Avalonia.Media.TextFormatting { var currentBreakPosition = 0; - var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(text); + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 517372648f..8afecb09e2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -162,17 +164,13 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in textRuns) { - if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0) + if (textRun.Text.IsEmpty) { - var characterBuffer = new CharacterBufferReference(new char[textRun.Length]); - - biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length)); + biDiData.Append(new char[textRun.Length]); } else { - var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); - - biDiData.Append(text); + biDiData.Append(textRun.Text.Span); } } @@ -198,9 +196,7 @@ namespace Avalonia.Media.TextFormatting case UnshapedTextRun shapeableRun: { var groupedRuns = new List(2) { shapeableRun }; - var characterBufferReference = currentRun.CharacterBufferReference; - var length = currentRun.Length; - var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; + var text = shapeableRun.Text; while (index + 1 < processedRuns.Count) { @@ -209,23 +205,14 @@ namespace Avalonia.Media.TextFormatting break; } - if (shapeableRun.CanShapeTogether(nextRun)) + if (shapeableRun.BidiLevel == nextRun.BidiLevel + && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) + && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) { groupedRuns.Add(nextRun); - - length += nextRun.Length; - - if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar) - { - offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar; - } - - characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter); - index++; - shapeableRun = nextRun; - + text = joinedText; continue; } @@ -237,7 +224,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); break; } @@ -253,12 +240,81 @@ namespace Avalonia.Media.TextFormatting return shapedRuns; } + /// + /// Tries to join two potnetially contiguous memory regions. + /// + /// The first memory region. + /// The second memory region. + /// On success, a memory region representing the union of the two regions. + /// true if the two regions were contigous; false otherwise. + private static bool TryJoinContiguousMemories(ReadOnlyMemory x, ReadOnlyMemory y, + out ReadOnlyMemory joinedMemory) + { + if (MemoryMarshal.TryGetString(x, out var xString, out var xStart, out var xLength)) + { + if (MemoryMarshal.TryGetString(y, out var yString, out var yStart, out var yLength) + && ReferenceEquals(xString, yString) + && TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart)) + { + joinedMemory = xString.AsMemory(joinedStart, xLength + yLength); + return true; + } + } + + else if (MemoryMarshal.TryGetArray(x, out var xSegment)) + { + if (MemoryMarshal.TryGetArray(y, out var ySegment) + && ReferenceEquals(xSegment.Array, ySegment.Array) + && TryGetContiguousStart(xSegment.Offset, xSegment.Count, ySegment.Offset, ySegment.Count, out var joinedStart)) + { + joinedMemory = xSegment.Array.AsMemory(joinedStart, xSegment.Count + ySegment.Count); + return true; + } + } + + else if (MemoryMarshal.TryGetMemoryManager(x, out MemoryManager? xManager, out xStart, out xLength)) + { + if (MemoryMarshal.TryGetMemoryManager(y, out MemoryManager? yManager, out var yStart, out var yLength) + && ReferenceEquals(xManager, yManager) + && TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart)) + { + joinedMemory = xManager.Memory.Slice(joinedStart, xLength + yLength); + return true; + } + } + + joinedMemory = default; + return false; + + static bool TryGetContiguousStart(int xStart, int xLength, int yStart, int yLength, out int joinedStart) + { + var xRange = (Start: xStart, Length: xLength); + var yRange = (Start: yStart, Length: yLength); + + var (firstRange, secondRange) = xStart <= yStart ? (xRange, yRange) : (yRange, xRange); + if (firstRange.Start + firstRange.Length == secondRange.Start) + { + joinedStart = firstRange.Start; + return true; + } + + joinedStart = default; + return false; + } + } + + + private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) + => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) + && x.Typeface == y.Typeface + && x.BaselineAlignment == y.BaselineAlignment; + private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) + IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) { var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); + var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) { @@ -294,7 +350,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; - CharacterBufferRange runText = default; + ReadOnlyMemory runText = default; for (var i = 0; i < textCharacters.Count; i++) { @@ -312,11 +368,12 @@ namespace Avalonia.Media.TextFormatting continue; } - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + runText = currentRun.Text; + var runTextSpan = runText.Span; - for (; j < runText.Length;) + for (; j < runTextSpan.Length;) { - Codepoint.ReadAt(runText, j, out var count); + Codepoint.ReadAt(runTextSpan, j, out var count); if (levelIndex + 1 == levels.Length) { @@ -326,9 +383,9 @@ namespace Avalonia.Media.TextFormatting levelIndex++; j += count; - if (j == runText.Length) + if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); runLevel = levels[levelIndex]; @@ -341,9 +398,10 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); - runText = runText.Skip(j); + runText = runText.Slice(j); + runTextSpan = runText.Span; j = 0; @@ -411,7 +469,7 @@ namespace Avalonia.Media.TextFormatting { if (TryGetLineBreak(textCharacters, out var runLineBreak)) { - var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap, + var splitResult = new TextCharacters(textCharacters.Text.Slice(0, runLineBreak.PositionWrap), textCharacters.Properties); textRuns.Add(splitResult); @@ -442,14 +500,14 @@ namespace Avalonia.Media.TextFormatting { lineBreak = default; - if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty) + var text = textRun.Text; + + if (text.IsEmpty) { return false; } - var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); - - var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange); + var lineBreakEnumerator = new LineBreakEnumerator(text.Span); while (lineBreakEnumerator.MoveNext()) { @@ -541,8 +599,7 @@ namespace Avalonia.Media.TextFormatting var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length); - var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; @@ -589,10 +646,8 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun: - { - var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(runText); + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (lineBreaker.MoveNext()) { @@ -651,9 +706,7 @@ namespace Avalonia.Media.TextFormatting currentRun = textRuns[index]; - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - lineBreaker = new LineBreakEnumerator(runText); + lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); } } else @@ -771,9 +824,7 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); - var characterBuffer = textRun.CharacterBufferReference; - - var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); + var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); return new ShapedTextRun(shapedBuffer, textRun.Properties); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 56232ec9c8..da343676bb 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Avalonia.Media.TextFormatting { @@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text run's text. /// - public virtual CharacterBufferReference CharacterBufferReference => default; + public virtual ReadOnlyMemory Text => default; /// /// A set of properties shared by every characters in the run @@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting _textRun = textRun; } - public string Text - { - get - { - unsafe - { - var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length); - - fixed (char* charsPtr = characterBuffer.Span) - { - return new string(charsPtr, 0, _textRun.Length); - } - } - } - } + public string Text => _textRun.Text.ToString(); public TextRunProperties? Properties => _textRun.Properties; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index c161b08d20..ae2a827944 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs @@ -40,14 +40,14 @@ namespace Avalonia.Media.TextFormatting } /// - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options = default) { - return _platformImpl.ShapeText(text, length, options); + return _platformImpl.ShapeText(text, options); } public ShapedBuffer ShapeText(string text, TextShaperOptions options = default) { - return ShapeText(new CharacterBufferReference(text), text.Length, options); + return ShapeText(text.AsMemory(), options); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 644f7e9a8a..0f0b3235e1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Appends text to the bidi data. /// /// The text to process. - public void Append(CharacterBufferRange text) + public void Append(ReadOnlySpan text) { _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 8b9d1c1d02..22f7b50fd4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -166,72 +166,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// - public static Codepoint ReadAt(IReadOnlyList text, int index, out int count) - { - count = 1; - - if (index >= text.Count) - { - return ReplacementCodepoint; - } - - var code = text[index]; - - ushort hi, low; - - //# High surrogate - if (0xD800 <= code && code <= 0xDBFF) - { - hi = code; - - if (index + 1 == text.Count) - { - return ReplacementCodepoint; - } - - low = text[index + 1]; - - if (0xDC00 <= low && low <= 0xDFFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } - - //# Low surrogate - if (0xDC00 <= code && code <= 0xDFFF) - { - if (index == 0) - { - return ReplacementCodepoint; - } - - hi = text[index - 1]; - - low = code; - - if (0xD800 <= hi && hi <= 0xDBFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } - - return new Codepoint(code); - } - - /// - /// Reads the at specified position. - /// - /// The buffer to read from. - /// The index to read at. - /// The count of character that were read. - /// - public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count) + public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) { count = 1; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index a2c36d9a13..d21f30ab7e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -4,9 +4,9 @@ namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private CharacterBufferRange _text; + private ReadOnlySpan _text; - public CodepointEnumerator(CharacterBufferRange text) + public CodepointEnumerator(ReadOnlySpan text) { _text = text; Current = Codepoint.ReplacementCodepoint; @@ -32,7 +32,7 @@ namespace Avalonia.Media.TextFormatting.Unicode Current = Codepoint.ReadAt(_text, 0, out var count); - _text = _text.Skip(count); + _text = _text.Slice(count); return true; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f75168083c..fa8e8ac976 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, int offset, int length) + public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) { FirstCodepoint = firstCodepoint; - Offset = offset; - Length = length; + Text = text; } /// @@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The Offset to the FirstCodepoint + /// The text of the grapheme cluster /// - public int Offset { get; } - - /// - /// The length of the grapheme cluster - /// - public int Length { get; } + public ReadOnlySpan Text { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index dc21e06813..812bb99d99 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -3,16 +3,16 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System.Collections.Generic; +using System; using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private CharacterBufferRange _text; + private ReadOnlySpan _text; - public GraphemeEnumerator(CharacterBufferRange text) + public GraphemeEnumerator(ReadOnlySpan text) { _text = text; Current = default; @@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode Return: - Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset); + Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); - _text = _text.Skip(processor.CurrentCodeUnitOffset); + _text = _text.Slice(processor.CurrentCodeUnitOffset); return true; // rules GB2, GB999 } @@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode [StructLayout(LayoutKind.Auto)] private ref struct Processor { - private readonly CharacterBufferRange _buffer; + private readonly ReadOnlySpan _buffer; private int _codeUnitLengthOfCurrentScalar; - internal Processor(CharacterBufferRange buffer) + internal Processor(ReadOnlySpan buffer) { _buffer = buffer; _codeUnitLengthOfCurrentScalar = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 41a476c17e..877ab76ce5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Collections.Generic; namespace Avalonia.Media.TextFormatting.Unicode { @@ -13,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public ref struct LineBreakEnumerator { - private readonly IReadOnlyList _text; + private readonly ReadOnlySpan _text; private int _position; private int _lastPosition; private LineBreakClass _currentClass; @@ -29,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private int _lb30a; private bool _lb31; - public LineBreakEnumerator(IReadOnlyList text) + public LineBreakEnumerator(ReadOnlySpan text) : this() { _text = text; @@ -63,7 +62,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30a = 0; } - while (_position < _text.Count) + while (_position < _text.Length) { _lastPosition = _position; var lastClass = _nextClass; @@ -93,11 +92,11 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - if (_position >= _text.Count) + if (_position >= _text.Length) { - if (_lastPosition < _text.Count) + if (_lastPosition < _text.Length) { - _lastPosition = _text.Count; + _lastPosition = _text.Length; var required = false; diff --git a/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs index 817086db88..5582ab4787 100644 --- a/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs @@ -1,4 +1,4 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting { @@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting /// public sealed class UnshapedTextRun : TextRun { - public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length, - TextRunProperties properties, sbyte biDiLevel) + public UnshapedTextRun(ReadOnlyMemory text, TextRunProperties properties, sbyte biDiLevel) { - CharacterBufferReference = characterBufferReference; - Length = length; + Text = text; Properties = properties; BidiLevel = biDiLevel; } - public override int Length { get; } + public override int Length + => Text.Length; - public override CharacterBufferReference CharacterBufferReference { get; } + public override ReadOnlyMemory Text { get; } public override TextRunProperties Properties { get; } public sbyte BidiLevel { get; } - - public bool CanShapeTogether(UnshapedTextRun unshapedTextRun) - { - if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference)) - { - return false; - } - - if (BidiLevel != unshapedTextRun.BidiLevel) - { - return false; - } - - if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, - unshapedTextRun.Properties.FontRenderingEmSize)) - { - return false; - } - - if (Properties.Typeface != unshapedTextRun.Properties.Typeface) - { - return false; - } - - if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment) - { - return false; - } - - return true; - } } } diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index c3eb89ab1a..a651b49e64 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -13,9 +14,8 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a shaped buffer. /// /// The text buffer. - /// The length of text. /// Text shaper options to customize the shaping process. /// A shaped glyph run. - ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); + ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options); } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 31ddf1f985..9bd1dc95f9 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -826,12 +826,12 @@ namespace Avalonia.Controls protected readonly record struct SimpleTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly TextRunProperties _defaultProperties; public SimpleTextSource(string text, TextRunProperties defaultProperties) { - _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); + _text = text; _defaultProperties = defaultProperties; } @@ -842,14 +842,14 @@ namespace Avalonia.Controls return new TextEndOfParagraph(); } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { return new TextEndOfParagraph(); } - return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); + return new TextCharacters(runText, _defaultProperties); } } @@ -884,14 +884,9 @@ namespace Avalonia.Controls if (textRun is TextCharacters) { - var characterBufferReference = textRun.CharacterBufferReference; - var skip = Math.Max(0, textSourceIndex - currentPosition); - return new TextCharacters( - new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip), - textRun.Length - skip, - textRun.Properties!); + return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!); } return textRun; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f388dc871e..9d07fb024a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -961,9 +961,7 @@ namespace Avalonia.Controls var length = 0; - var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); - - var graphemeEnumerator = new GraphemeEnumerator(inputRange); + var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan()); while (graphemeEnumerator.MoveNext()) { @@ -981,7 +979,7 @@ namespace Avalonia.Controls } } - length += grapheme.Length; + length += grapheme.Text.Length; } if (length < input.Length) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 52815b943d..c1146cceda 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -79,12 +79,10 @@ namespace Avalonia.Controls { if(run.Length > 0) { - var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length); - #if NET6_0 - builder.Append(characterBufferRange.Span); + builder.Append(run.Text.Span); #else - builder.Append(characterBufferRange.Span.ToArray()); + builder.Append(run.Text.Span.ToArray()); #endif } } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 1cc0fa73bb..2a04e624cb 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -145,15 +145,13 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - var characterBufferRange = new CharacterBufferRange(text, length); - - return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); + return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 98eb35d5c5..e0f95bac60 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,9 +13,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { - var text = new CharacterBufferRange(characterBufferReference, length); + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -21,7 +23,9 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { - buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); @@ -64,7 +68,7 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (text[i] == '\t') + if (textSpan[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -147,5 +151,26 @@ namespace Avalonia.Skia // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 6685dd00b9..fffa5ce490 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,8 +13,9 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -20,7 +23,9 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { - buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); @@ -34,7 +39,7 @@ namespace Avalonia.Direct2D1.Media font.Shape(buffer); - if(buffer.Direction == Direction.RightToLeft) + if (buffer.Direction == Direction.RightToLeft) { buffer.Reverse(); } @@ -45,9 +50,7 @@ namespace Avalonia.Direct2D1.Media var bufferLength = buffer.Length; - var characterBufferRange = new CharacterBufferRange(characterBufferReference, length); - - var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -61,11 +64,11 @@ namespace Avalonia.Direct2D1.Media var glyphCluster = (int)(sourceInfo.Cluster); - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (characterBufferRange[i] == '\t') + if (textSpan[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -148,5 +151,26 @@ namespace Avalonia.Direct2D1.Media // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index b2c40f4ff1..9d189d1950 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); // Append - bidiData.Append(new CharacterBufferRange(text)); + bidiData.Append(text); // Act for (int i = 0; i < 10; i++) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index c57bd6c002..a022039000 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -38,18 +38,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Codepoints).ToArray()); var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Grapheme).ToArray()).AsSpan(); - var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var enumerator = new GraphemeEnumerator(text); enumerator.MoveNext(); - var actual = text.AsSpan(enumerator.Current.Offset, enumerator.Current.Length); + var actual = enumerator.Current.Text; - var pass = true; - - if(actual.Length != grapheme.Length) - { - pass = false; - } + bool pass = actual.Length == grapheme.Length; if (pass) { @@ -87,13 +82,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var enumerator = new GraphemeEnumerator(text); var count = 0; while (enumerator.MoveNext()) { - Assert.Equal(1, enumerator.Current.Length); + Assert.Equal(1, enumerator.Current.Text.Length); count++; } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index b2648bf348..d198fe81a6 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void BasicLatinTest() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test.")); + var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test."); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); @@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTextWithOuterWhitespace() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas ")); + var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas "); var positionsF = GetBreaks(lineBreaker); Assert.Equal(1, positionsF[0].PositionWrap); Assert.Equal(0, positionsF[0].PositionMeasure); @@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTest() { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas")); + var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas"); var positionsF = GetBreaks(lineBreaker); Assert.Equal(7, positionsF[0].PositionWrap); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); + var lineBreaker = new LineBreakEnumerator(text); var foundBreaks = new List(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 8e06fbd831..61ce056c49 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotNull(target.TextLayout); var actual = string.Join(null, - target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString())); + target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.ToString())); Assert.Equal("****", actual); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 4083a67b5e..04bc401479 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -60,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -103,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); + TextShaper.Current.ShapeText(text, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -112,14 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media var characterHit = glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); - Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } else { var characterHit = glyphRun.GetCharacterHitFromDistance(0, out _); - Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } var rects = BuildRects(glyphRun); @@ -215,7 +215,7 @@ namespace Avalonia.Skia.UnitTests.Media var glyphRun = new GlyphRun( shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, - shapedBuffer.CharacterBufferRange, + shapedBuffer.Text, shapedBuffer.GlyphIndices, shapedBuffer.GlyphAdvances, shapedBuffer.GlyphOffsets, diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index f12f42bd5e..f963277397 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -1,15 +1,16 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { internal class SingleBufferTextSource : ITextSource { - private readonly CharacterBufferRange _text; + private readonly string _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) { - _text = new CharacterBufferRange(text); + _text = text; _defaultGenericPropertiesRunProperties = defaultProperties; } @@ -20,14 +21,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return null; } - var runText = _text.Skip(textSourceIndex); + var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { return null; } - return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties); + return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 1b6fd537eb..b90752861c 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -279,7 +279,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); + var lineBreaker = new LineBreakEnumerator(text); var expected = new List(); @@ -677,7 +677,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green); } - return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); + return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7501bf21fe..1fd26748cd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; @@ -62,7 +63,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("1 ", actual); @@ -144,8 +145,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); - var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); - var inner = new GraphemeEnumerator(new CharacterBufferRange(text)); + var outer = new GraphemeEnumerator(text); + var inner = new GraphemeEnumerator(text); var i = 0; var j = 0; @@ -153,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { while (inner.MoveNext()) { - j += inner.Current.Length; + j += inner.Current.Text.Length; if (j + i > text.Length) { @@ -190,9 +191,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting break; } - inner = new GraphemeEnumerator(new CharacterBufferRange(text)); + inner = new GraphemeEnumerator(text); - i += outer.Current.Length; + i += outer.Current.Text.Length; } } } @@ -261,7 +262,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("89", actual); @@ -331,7 +332,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textRun.Length); - var actual = new CharacterBufferRange(textRun).Span.ToString(); + var actual = textRun.Text.ToString(); Assert.Equal("😄", actual); @@ -667,10 +668,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(5, layout.TextLines.Count); - Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0])); - Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0])); - Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0])); - Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0])); + Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text.ToString()); + Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text.ToString()); + Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text.ToString()); + Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text.ToString()); } } @@ -815,8 +816,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(textLine.Width <= maxWidth); var actual = new string(textLine.TextRuns.Cast() - .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) - .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); + .OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)) + .SelectMany(x => x.Text.ToString()) + .ToArray()); var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); @@ -968,15 +970,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var i = 0; - var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); + var graphemeEnumerator = new GraphemeEnumerator(text); while (graphemeEnumerator.MoveNext()) { var grapheme = graphemeEnumerator.Current; - var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - i += grapheme.Length; + i += grapheme.Text.Length; var layout = new TextLayout( text, @@ -1020,6 +1022,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 2c6ccfa896..6993c70e8b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -90,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; @@ -137,7 +138,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; @@ -410,7 +411,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(collapsedLine.HasCollapsed); - var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray(); + var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text.ToString()).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); @@ -653,7 +654,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var run = textRuns[i]; var bounds = runBounds[i]; - Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex); + Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 63e0083b1d..834dce4a90 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture); var shapedBuffer = TextShaper.Current.ShapeText(text, options); - Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length); + Assert.Equal(shapedBuffer.Length, text.Length); Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(1, shapedBuffer.GlyphClusters[1]); diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index ae7e00aca1..566cb0f1ac 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -1,18 +1,21 @@ using System; +using System.Buffers; using System.Globalization; +using System.Runtime.InteropServices; 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.UnitTests { - public class HarfBuzzTextShaperImpl : ITextShaperImpl + internal class HarfBuzzTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { + var textSpan = text.Span; var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -20,15 +23,17 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { - buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength); + // HarfBuzz needs the surrounding characters to correctly shape the text + var containingText = GetContainingMemory(text, out var start, out var length); + buffer.AddUtf16(containingText.Span, start, length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font; @@ -45,9 +50,7 @@ namespace Avalonia.UnitTests var bufferLength = buffer.Length; - var characterBufferRange = new CharacterBufferRange(text, textLength); - - var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -59,12 +62,21 @@ namespace Avalonia.UnitTests var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); - var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); + var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); + if (textSpan[i] == '\t') + { + glyphIndex = typeface.GetGlyph(' '); + + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : + 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; + } + var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; @@ -79,7 +91,7 @@ namespace Avalonia.UnitTests var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint(second.Codepoint).IsBreakChar) @@ -90,19 +102,19 @@ namespace Avalonia.UnitTests 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; unsafe { - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 2]) + fixed (GlyphInfo* p = &glyphInfos[length - 2]) { *p = first; } - - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1]) + + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; } @@ -114,7 +126,7 @@ namespace Avalonia.UnitTests unsafe { - fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1]) + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; } @@ -136,8 +148,29 @@ namespace Avalonia.UnitTests private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) { // Depends on direction of layout - // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } + + private static ReadOnlyMemory GetContainingMemory(ReadOnlyMemory memory, out int start, out int length) + { + if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) + { + return containingString.AsMemory(); + } + + if (MemoryMarshal.TryGetArray(memory, out var segment)) + { + return segment.Array.AsMemory(); + } + + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + { + return memoryManager.Memory; + } + + // should never happen + throw new InvalidOperationException("Memory not backed by string, array or manager"); + } } } diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 00bcef295a..3218139251 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.TextFormatting; +using System; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -6,19 +7,20 @@ namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - var characterBufferRange = new CharacterBufferRange(text, length); - var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); + var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var textSpan = text.Span; + var textStartIndex = TextTestHelper.GetStartCharIndex(text); for (var i = 0; i < shapedBuffer.Length;) { - var glyphCluster = i + text.OffsetToFirstChar; + var glyphCluster = i + textStartIndex; - var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count); + var codepoint = Codepoint.ReadAt(textSpan, i, out var count); var glyphIndex = typeface.GetGlyph(codepoint); diff --git a/tests/Avalonia.UnitTests/TextTestHelper.cs b/tests/Avalonia.UnitTests/TextTestHelper.cs new file mode 100644 index 0000000000..b572333027 --- /dev/null +++ b/tests/Avalonia.UnitTests/TextTestHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.UnitTests +{ + public static class TextTestHelper + { + public static int GetStartCharIndex(ReadOnlyMemory text) + { + if (!MemoryMarshal.TryGetString(text, out _, out var start, out _)) + throw new InvalidOperationException("text memory should have been a string"); + return start; + } + } +}