From 895d85aa89a1b80ea6dc6c033c44913b744de883 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 8 Dec 2022 14:01:09 +0100 Subject: [PATCH 1/2] Implement CharacterBufferReference and related classes --- .../Pages/TextFormatterPage.axaml.cs | 2 +- src/Avalonia.Base/Media/FormattedText.cs | 8 +- src/Avalonia.Base/Media/GlyphRun.cs | 419 ++++++++++-------- src/Avalonia.Base/Media/GlyphRunMetrics.cs | 18 +- .../TextFormatting/CharacterBufferRange.cs | 308 +++++++++++++ .../CharacterBufferReference.cs | 176 ++++++++ .../TextFormatting/FormattedTextSource.cs | 13 +- .../TextFormatting/InterWordJustification.cs | 19 +- .../TextFormatting/ShapeableTextCharacters.cs | 20 +- .../Media/TextFormatting/ShapedBuffer.cs | 31 +- .../TextFormatting/ShapedTextCharacters.cs | 19 +- .../Media/TextFormatting/SplitResult.cs | 2 +- .../Media/TextFormatting/TextCharacters.cs | 144 ++++-- .../TextFormatting/TextEllipsisHelper.cs | 102 ++--- .../Media/TextFormatting/TextEndOfLine.cs | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 100 +++-- .../Media/TextFormatting/TextLayout.cs | 2 +- .../TextLeadingPrefixCharacterEllipsis.cs | 5 +- .../Media/TextFormatting/TextLineImpl.cs | 358 ++++++++------- .../Media/TextFormatting/TextLineMetrics.cs | 6 +- .../Media/TextFormatting/TextMetrics.cs | 4 +- .../Media/TextFormatting/TextRun.cs | 11 +- .../Media/TextFormatting/TextShaper.cs | 11 +- .../TextTrailingCharacterEllipsis.cs | 3 +- .../TextTrailingWordEllipsis.cs | 2 +- .../Media/TextFormatting/Unicode/BiDiData.cs | 3 +- .../Media/TextFormatting/Unicode/Codepoint.cs | 9 +- .../Unicode/CodepointEnumerator.cs | 7 +- .../Media/TextFormatting/Unicode/Grapheme.cs | 8 +- .../Unicode/GraphemeEnumerator.cs | 12 +- .../Unicode/LineBreakEnumerator.cs | 15 +- .../Media/TextLeadingPrefixTrimming.cs | 11 +- .../Media/TextTrailingTrimming.cs | 11 +- src/Avalonia.Base/Media/TextTrimming.cs | 2 +- src/Avalonia.Base/Platform/ITextShaperImpl.cs | 5 +- .../Composition/Server/FpsCounter.cs | 3 +- src/Avalonia.Base/Utilities/ArraySlice.cs | 8 - src/Avalonia.Base/Utilities/ReadOnlySlice.cs | 239 ---------- src/Avalonia.Controls/Documents/LineBreak.cs | 4 +- src/Avalonia.Controls/Documents/Run.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 27 +- src/Avalonia.Controls/TextBox.cs | 4 +- .../TextBoxTextInputMethodClient.cs | 8 +- .../HeadlessPlatformStubs.cs | 6 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 24 +- .../Media/TextShaperImpl.cs | 11 +- .../Media/GlyphRunTests.cs | 4 +- .../Media/TextFormatting/BiDiClassTests.cs | 3 +- .../GraphemeBreakClassTrieGeneratorTests.cs | 9 +- .../LineBreakEnumuratorTests.cs | 9 +- .../Utilities/ReadOnlySpanTests.cs | 37 -- .../Presenters/TextPresenter_Tests.cs | 2 +- .../Media/GlyphRunTests.cs | 29 +- .../TextFormatting/MultiBufferTextSource.cs | 3 +- .../TextFormatting/SingleBufferTextSource.cs | 19 +- .../TextFormatting/TextFormatterTests.cs | 21 +- .../Media/TextFormatting/TextLayoutTests.cs | 46 +- .../Media/TextFormatting/TextLineTests.cs | 90 ++-- .../Media/TextFormatting/TextShaperTests.cs | 6 +- .../HarfBuzzTextShaperImpl.cs | 8 +- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 12 +- 61 files changed, 1427 insertions(+), 1077 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs create mode 100644 src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs delete mode 100644 src/Avalonia.Base/Utilities/ReadOnlySlice.cs delete mode 100644 tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs index 57a5c7101f..8fbfa854b1 100644 --- a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs +++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs @@ -90,7 +90,7 @@ namespace RenderDemo.Pages return new ControlRun(_control, _defaultProperties); } - return new TextCharacters(_text.AsMemory(), _defaultProperties); + return new TextCharacters(_text, _defaultProperties); } } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 90b9755493..138e8b79eb 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1,10 +1,8 @@ using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; -using Avalonia.Controls; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; @@ -25,7 +23,7 @@ namespace Avalonia.Media private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm; // properties and format runs - private ReadOnlySlice _text; + private string _text; private readonly SpanVector _formatRuns = new SpanVector(null); private SpanPosition _latestPosition; @@ -69,9 +67,7 @@ namespace Avalonia.Media ValidateFontSize(emSize); - _text = textToFormat != null ? - new ReadOnlySlice(textToFormat.AsMemory()) : - throw new ArgumentNullException(nameof(textToFormat)); + _text = textToFormat; var runProps = new GenericTextRunProperties( typeface, diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index d93a68e78b..af9e458a28 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -22,15 +21,12 @@ namespace Avalonia.Media private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlySlice _characters; - + private IReadOnlyList _characters; private IReadOnlyList _glyphIndices; private IReadOnlyList? _glyphAdvances; private IReadOnlyList? _glyphOffsets; private IReadOnlyList? _glyphClusters; - private int _offsetToFirstCharacter; - /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -45,7 +41,7 @@ namespace Avalonia.Media public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - ReadOnlySlice characters, + IReadOnlyList characters, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances = null, IReadOnlyList? glyphOffsets = null, @@ -54,19 +50,19 @@ namespace Avalonia.Media { _glyphTypeface = glyphTypeface; - FontRenderingEmSize = fontRenderingEmSize; + _fontRenderingEmSize = fontRenderingEmSize; - Characters = characters; + _characters = characters; _glyphIndices = glyphIndices; - GlyphAdvances = glyphAdvances; + _glyphAdvances = glyphAdvances; - GlyphOffsets = glyphOffsets; + _glyphOffsets = glyphOffsets; - GlyphClusters = glyphClusters; + _glyphClusters = glyphClusters; - BiDiLevel = biDiLevel; + _biDiLevel = biDiLevel; } /// @@ -145,7 +141,7 @@ namespace Avalonia.Media /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// - public ReadOnlySlice Characters + public IReadOnlyList Characters { get => _characters; set => Set(ref _characters, value); @@ -219,7 +215,7 @@ namespace Avalonia.Media /// public double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var distance = 0.0; @@ -227,12 +223,12 @@ namespace Avalonia.Media { if (GlyphClusters != null) { - if (characterIndex < GlyphClusters[0]) + if (characterIndex < Metrics.FirstCluster) { return 0; } - if (characterIndex > GlyphClusters[GlyphClusters.Count - 1]) + if (characterIndex > Metrics.LastCluster) { return Metrics.WidthIncludingTrailingWhitespace; } @@ -268,12 +264,12 @@ namespace Avalonia.Media if (GlyphClusters != null && GlyphClusters.Count > 0) { - if (characterIndex > GlyphClusters[0]) + if (characterIndex > Metrics.LastCluster) { return 0; } - if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1]) + if (characterIndex <= Metrics.FirstCluster) { return Size.Width; } @@ -299,19 +295,12 @@ namespace Avalonia.Media /// public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) { - var characterIndex = 0; - // Before if (distance <= 0) { isInside = false; - if (GlyphClusters != null) - { - characterIndex = GlyphClusters[characterIndex]; - } - - var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _); + var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _); return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; } @@ -321,18 +310,13 @@ namespace Avalonia.Media { isInside = false; - characterIndex = GlyphIndices.Count - 1; - - if (GlyphClusters != null) - { - characterIndex = GlyphClusters[characterIndex]; - } - - var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _); + var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); } + var characterIndex = 0; + //Within var currentX = 0d; @@ -378,7 +362,7 @@ namespace Avalonia.Media var characterHit = FindNearestCharacterHit(characterIndex, out var width); var delta = width / 2; - + var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3); var isTrailing = offset > delta; @@ -400,24 +384,15 @@ namespace Avalonia.Media { characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); - var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - return textPosition > _characters.End ? - characterHit : - new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength); - } - - var nextCharacterHit = - FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + if (characterHit.FirstCharacterIndex == Metrics.LastCluster) + { + return characterHit; + } - if (characterHit == nextCharacterHit) - { - return characterHit; + return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength); } - return characterHit.TrailingLength > 0 ? - nextCharacterHit : - new CharacterHit(nextCharacterHit.FirstCharacterIndex); + return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); } /// @@ -454,29 +429,24 @@ namespace Avalonia.Media return characterIndex; } - if (IsLeftToRight) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex < GlyphClusters[0]) + if (IsLeftToRight) { - return 0; + return GlyphIndices.Count - 1; } - if (characterIndex > GlyphClusters[GlyphClusters.Count - 1]) - { - return GlyphClusters.Count - 1; - } + return 0; } - else - { - if (characterIndex < GlyphClusters[GlyphClusters.Count - 1]) - { - return GlyphClusters.Count - 1; - } - if (characterIndex > GlyphClusters[0]) + if (characterIndex < Metrics.FirstCluster) + { + if (IsLeftToRight) { return 0; } + + return GlyphIndices.Count - 1; } var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; @@ -498,7 +468,7 @@ namespace Avalonia.Media if (start < 0) { - return -1; + goto result; } } @@ -517,6 +487,18 @@ namespace Avalonia.Media } } + result: + + if (start < 0) + { + return 0; + } + + if (start > GlyphIndices.Count - 1) + { + return GlyphIndices.Count - 1; + } + return start; } @@ -532,20 +514,20 @@ namespace Avalonia.Media { width = 0.0; - var start = FindGlyphIndex(index); + var glyphIndex = FindGlyphIndex(index); if (GlyphClusters == null) { width = GetGlyphAdvance(index, out _); - return new CharacterHit(start, 1); + return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[start]; + var cluster = GlyphClusters[glyphIndex]; var nextCluster = cluster; - var currentIndex = start; + var currentIndex = glyphIndex; while (nextCluster == cluster) { @@ -571,20 +553,64 @@ namespace Avalonia.Media } nextCluster = GlyphClusters[currentIndex]; - } + } - int trailingLength; + var clusterLength = Math.Max(0, nextCluster - cluster); - if (nextCluster == cluster) - { - trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster; - } - else + if (cluster == Metrics.LastCluster && clusterLength == 0) { - trailingLength = nextCluster - cluster; + var characterLength = 0; + + var currentCluster = Metrics.FirstCluster; + + if (IsLeftToRight) + { + for (int i = 1; i < GlyphClusters.Count; i++) + { + nextCluster = GlyphClusters[i]; + + if (currentCluster > cluster) + { + break; + } + + var length = nextCluster - currentCluster; + + characterLength += length; + + currentCluster = nextCluster; + } + } + else + { + for (int i = GlyphClusters.Count - 1; i >= 0; i--) + { + nextCluster = GlyphClusters[i]; + + if (currentCluster > cluster) + { + break; + } + + var length = nextCluster - currentCluster; + + characterLength += length; + + currentCluster = nextCluster; + } + } + + if (Characters != null) + { + clusterLength = Characters.Count - characterLength; + } + else + { + clusterLength = 1; + } } - return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength); + return new CharacterHit(cluster, clusterLength); } /// @@ -618,22 +644,25 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - var firstCluster = 0; - var lastCluster = Characters.Length - 1; + int firstCluster = 0, lastCluster = 0; - if (!IsLeftToRight) + if (_glyphClusters != null && _glyphClusters.Count > 0) { - var cluster = firstCluster; - firstCluster = lastCluster; - lastCluster = cluster; + firstCluster = _glyphClusters[0]; + lastCluster = _glyphClusters[_glyphClusters.Count - 1]; } - - if (GlyphClusters != null && GlyphClusters.Count > 0) + else { - firstCluster = GlyphClusters[0]; - lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + if (Characters != null && Characters.Count > 0) + { + firstCluster = 0; + lastCluster = Characters.Count - 1; + } + } - _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); + if (!IsLeftToRight) + { + (lastCluster, firstCluster) = (firstCluster, lastCluster); } var isReversed = firstCluster > lastCluster; @@ -666,12 +695,19 @@ namespace Avalonia.Media } } - return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength, - height); + return new GlyphRunMetrics( + width, + widthIncludingTrailingWhitespace, + height, + trailingWhitespaceLength, + newLineLength, + firstCluster, + lastCluster + ); } private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) - { + { if (isReversed) { return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); @@ -681,66 +717,82 @@ namespace Avalonia.Media newLineLength = 0; var trailingWhitespaceLength = 0; - if (GlyphClusters == null) + if (Characters != null) { - for (var i = _characters.Length - 1; i >= 0;) + if (GlyphClusters == null) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); - - if (!codepoint.IsWhiteSpace) + for (var i = _characters.Count - 1; i >= 0;) { - break; - } + var codepoint = Codepoint.ReadAt(_characters, i, out var count); - if (codepoint.IsBreakChar) - { - newLineLength++; - } + if (!codepoint.IsWhiteSpace) + { + break; + } - trailingWhitespaceLength++; + if (codepoint.IsBreakChar) + { + newLineLength++; + } + + trailingWhitespaceLength++; - i -= count; - glyphCount++; + i -= count; + glyphCount++; + } } - } - else - { - for (var i = GlyphClusters.Count - 1; i >= 0; i--) + else { - var currentCluster = GlyphClusters[i]; - var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); - - if (!codepoint.IsWhiteSpace) + if (Characters.Count > 0) { - break; - } + var characterIndex = Characters.Count - 1; - var clusterLength = 1; + for (var i = GlyphClusters.Count - 1; i >= 0; i--) + { + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); - while(i - 1 >= 0) - { - var nextCluster = GlyphClusters[i - 1]; + characterIndex -= characterLength; - if(currentCluster == nextCluster) - { - clusterLength++; - i--; + if (!codepoint.IsWhiteSpace) + { + break; + } - continue; - } + var clusterLength = 1; - break; - } + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; - } + if (currentCluster == nextCluster) + { + clusterLength++; + i--; + + if(characterIndex >= 0) + { + codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength); + + characterIndex -= characterLength; + } + + continue; + } + + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } - trailingWhitespaceLength += clusterLength; - - glyphCount++; + trailingWhitespaceLength += clusterLength; + + glyphCount++; + } + } } } @@ -753,67 +805,73 @@ namespace Avalonia.Media newLineLength = 0; var trailingWhitespaceLength = 0; - if (GlyphClusters == null) + if (Characters != null) { - for (var i = 0; i < Characters.Length;) + if (GlyphClusters == null) { - var codepoint = Codepoint.ReadAt(_characters, i, out var count); - - if (!codepoint.IsWhiteSpace) + for (var i = 0; i < Characters.Count;) { - break; - } + var codepoint = Codepoint.ReadAt(_characters, i, out var count); - if (codepoint.IsBreakChar) - { - newLineLength++; - } + if (!codepoint.IsWhiteSpace) + { + break; + } - trailingWhitespaceLength++; + if (codepoint.IsBreakChar) + { + newLineLength++; + } - i += count; - glyphCount++; + trailingWhitespaceLength++; + + i += count; + glyphCount++; + } } - } - else - { - for (var i = 0; i < GlyphClusters.Count; i++) + else { - var currentCluster = GlyphClusters[i]; - var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); - var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) + for (var i = 0; i < GlyphClusters.Count; i++) { - break; - } + var currentCluster = GlyphClusters[i]; + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength); - var clusterLength = 1; + characterIndex += characterLength; - var j = i; + if (!codepoint.IsWhiteSpace) + { + break; + } - while (j - 1 >= 0) - { - var nextCluster = GlyphClusters[--j]; + var clusterLength = 1; - if (currentCluster == nextCluster) + var j = i; + + while (j - 1 >= 0) { - clusterLength++; + var nextCluster = GlyphClusters[--j]; - continue; - } + if (currentCluster == nextCluster) + { + clusterLength++; - break; - } + continue; + } - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; - } + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } - trailingWhitespaceLength += clusterLength; + trailingWhitespaceLength += clusterLength; - glyphCount += clusterLength; + glyphCount += clusterLength; + } } } @@ -855,14 +913,9 @@ namespace Avalonia.Media throw new InvalidOperationException(); } - _glyphRunImpl = CreateGlyphRunImpl(); - } - - private IGlyphRunImpl CreateGlyphRunImpl() - { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index a8698a7d82..983f029c7a 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,24 +2,30 @@ { public readonly struct GlyphRunMetrics { - public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength, - int newlineLength, double height) + public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, + int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) { Width = width; WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - TrailingWhitespaceLength = trailingWhitespaceLength; - NewlineLength = newlineLength; Height = height; + TrailingWhitespaceLength = trailingWhitespaceLength; + NewLineLength= newLineLength; + FirstCluster = firstCluster; + LastCluster = lastCluster; } public double Width { get; } public double WidthIncludingTrailingWhitespace { get; } + public double Height { get; } + public int TrailingWhitespaceLength { get; } - public int NewlineLength { get; } + public int NewLineLength { get; } - public double Height { get; } + public int FirstCluster { get; } + + public int LastCluster { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs new file mode 100644 index 0000000000..045f336700 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -0,0 +1,308 @@ +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 from unsafe character string + /// + /// pointer to character string + /// character length + public unsafe CharacterBufferRange( + char* unsafeCharacterString, + int characterLength + ) + : this( + new CharacterBufferReference(unsafeCharacterString, characterLength), + 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 Span[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 => + CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + + /// + /// Gets the character memory buffer + /// + internal ReadOnlyMemory CharacterBuffer + { + get { return CharacterBufferReference.CharacterBuffer; } + } + + /// + /// Gets the character offset relative to the beginning of buffer to + /// the first character of the run + /// + internal int OffsetToFirstChar + { + get { return CharacterBufferReference.OffsetToFirstChar; } + } + + /// + /// Indicate whether the character buffer range is empty + /// + internal bool IsEmpty + { + get { return CharacterBufferReference.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( + CharacterBufferReference.CharacterBuffer, + CharacterBufferReference.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() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs new file mode 100644 index 0000000000..a15562cb52 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs @@ -0,0 +1,176 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +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 unsafe character string + /// + /// pointer to character string + /// character length of unsafe string + public unsafe CharacterBufferReference(char* unsafeCharacterString, int characterLength) + : this(new UnmanagedMemoryManager(unsafeCharacterString, characterLength).Memory, 0) + { } + + /// + /// 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; + } + + /// + /// 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); + } + + public ReadOnlyMemory CharacterBuffer { get; } + + public int OffsetToFirstChar { get; } + + /// + /// A MemoryManager over a raw pointer + /// + /// The pointer is assumed to be fully unmanaged, or externally pinned - no attempt will be made to pin this data + public sealed unsafe class UnmanagedMemoryManager : MemoryManager + where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + /// + /// Create a new UnmanagedMemoryManager instance at the given pointer and size + /// + /// It is assumed that the span provided is already unmanaged or externally pinned + public UnmanagedMemoryManager(Span span) + { + fixed (T* ptr = &MemoryMarshal.GetReference(span)) + { + _pointer = ptr; + _length = span.Length; + } + } + /// + /// Create a new UnmanagedMemoryManager instance at the given pointer and size + /// + public UnmanagedMemoryManager(T* pointer, int length) + { + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + _pointer = pointer; + _length = length; + } + /// + /// Obtains a span that represents the region + /// + public override Span GetSpan() => new Span(_pointer, _length); + + /// + /// Provides access to a pointer that represents the data (note: no actual pin occurs) + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0 || elementIndex >= _length) + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + return new MemoryHandle(_pointer + elementIndex); + } + /// + /// Has no effect + /// + public override void Unpin() { } + + /// + /// Releases all resources associated with this object + /// + protected override void Dispose(bool disposing) { } + } + } +} + diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index fb8e699d8e..e745a873a2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting { internal readonly struct FormattedTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; + private readonly int length; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; - public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties, + public FormattedTextSource(string text, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { - _text = text; + _text = new CharacterBufferRange(text); _defaultProperties = defaultProperties; _textModifier = textModifier; } @@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value); + return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value); } /// @@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting /// /// The created text style run. /// - private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex, + private static ValueSpan CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { if (textModifier == null || textModifier.Count == 0) @@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting return new ValueSpan(firstTextSourceIndex, length, currentProperties); } - private static int CoerceLength(ReadOnlySlice text, int length) + private static int CoerceLength(CharacterBufferRange text, int length) { var finalLength = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index a49e4ef13b..3c3a46c209 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); + var currentPosition = textLine.FirstTextSourceIndex; + foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.Text; + var text = new CharacterBufferRange(textRun); if (text.IsEmpty) { continue; } - var start = text.Start; - var lineBreakEnumerator = new LineBreakEnumerator(text); while (lineBreakEnumerator.MoveNext()) { var currentBreak = lineBreakEnumerator.Current; - if (!currentBreak.Required && currentBreak.PositionWrap != text.Length) + if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) { - breakOportunities.Enqueue(start + currentBreak.PositionMeasure); + breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); } } + + currentPosition += textRun.Length; } if (breakOportunities.Count == 0) @@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); var spacing = remainingSpace / breakOportunities.Count; + currentPosition = textLine.FirstTextSourceIndex; + foreach (var textRun in lineImpl.TextRuns) { - var text = textRun.Text; + var text = textRun.CharacterBufferReference.CharacterBuffer; if (text.IsEmpty) { @@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting { var glyphRun = shapedText.GlyphRun; var shapedBuffer = shapedText.ShapedBuffer; - var currentPosition = text.Start; while (breakOportunities.Count > 0) { @@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; } + + currentPosition += textRun.Length; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs index b31a6f4d13..0e8d6e3e4a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs @@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting /// public sealed class ShapeableTextCharacters : TextRun { - public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties, sbyte biDiLevel) + public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length, + TextRunProperties properties, sbyte biDiLevel) { - TextSourceLength = text.Length; - Text = text; + CharacterBufferReference = characterBufferReference; + Length = length; Properties = properties; BidiLevel = biDiLevel; } - public override int TextSourceLength { get; } + public override int Length { get; } - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } public override TextRunProperties Properties { get; } - + public sbyte BidiLevel { get; } public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) { - if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer)) - { - return false; - } - - if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start) + if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference)) { return false; } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 85924a3d32..644c0ecbe1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList { private static readonly IComparer s_clusterComparer = new CompareClusters(); - - public ShapedBuffer(ReadOnlySlice text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) - : this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel) + + public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : + this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) { } - internal ShapedBuffer(ReadOnlySlice text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - Text = text; + CharacterBufferRange = characterBufferRange; GlyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting } internal ArraySlice GlyphInfos { get; } - - public ReadOnlySlice Text { get; } - + public int Length => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); + public CharacterBufferRange CharacterBufferRange { get; } + /// /// Finds a glyph index for given character index. /// @@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - if (Text.Length == length) + if (CharacterBufferRange.Length == length) { return new SplitResult(this, null); } - var glyphCount = FindGlyphIndex(Text.Start + length); + var firstCluster = GlyphClusters[0]; + var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + + var start = firstCluster < lastCluster ? firstCluster : lastCluster; + + var glyphCount = FindGlyphIndex(start + length); - var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + var first = new ShapedBuffer(CharacterBufferRange.Take(length), + GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); - var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + var second = new ShapedBuffer(CharacterBufferRange.Skip(length), + GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs index 21101f462c..3035eb7b18 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties) { ShapedBuffer = shapedBuffer; - Text = shapedBuffer.Text; + CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; + Length = shapedBuffer.CharacterBufferRange.Length; Properties = properties; - TextSourceLength = Text.Length; - TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } @@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting public ShapedBuffer ShapedBuffer { get; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } /// - public override int TextSourceLength { get; } + public override int Length { get; } public TextMetrics TextMetrics { get; } @@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting #if DEBUG - if (first.Text.Length != length) + if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - - #endif + +#endif var second = new ShapedTextCharacters(splitBuffer.Second!, Properties); @@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting return new GlyphRun( ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, - Text, + new CharacterBufferRange(CharacterBufferReference, Length), ShapedBuffer.GlyphIndices, ShapedBuffer.GlyphAdvances, ShapedBuffer.GlyphOffsets, diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 02c7174499..03b93cfaf0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media.TextFormatting { - internal readonly struct SplitResult + public readonly struct SplitResult { public SplitResult(T first, T? second) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index bcfa35ae30..9587786c5b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -10,26 +9,98 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { - public TextCharacters(ReadOnlySlice text, TextRunProperties properties) - { - TextSourceLength = text.Length; - Text = text; - Properties = properties; - } + /// + /// Construct a run of text content from character array + /// + 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(ReadOnlySlice text, int offsetToFirstCharacter, int length, - TextRunProperties properties) + /// + /// Construct a run for text content from unsafe character string + /// + public unsafe TextCharacters( + char* unsafeCharacterString, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(unsafeCharacterString, length), + length, + textRunProperties + ) + { } + + /// + /// Internal constructor of TextContent + /// + public TextCharacters( + CharacterBufferReference characterBufferReference, + int length, + TextRunProperties textRunProperties + ) { - Text = text.Skip(offsetToFirstCharacter).Take(length); - TextSourceLength = length; - Properties = properties; + if (length <= 0) + { + throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero"); + } + + if (textRunProperties.FontRenderingEmSize <= 0) + { + throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero"); + } + + CharacterBufferReference = characterBufferReference; + Length = length; + Properties = textRunProperties; } /// - public override int TextSourceLength { get; } + public override int Length { get; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } @@ -38,18 +109,17 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); - while (!runText.IsEmpty) + while (characterBufferRange.Length > 0) { - var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties); shapeableCharacters.Add(shapeableRun); - runText = runText.Skip(shapeableRun.Text.Length); + characterBufferRange = characterBufferRange.Skip(shapeableRun.Length); previousProperties = shapeableRun.Properties; } @@ -60,45 +130,45 @@ namespace Avalonia.Media.TextFormatting /// /// Creates a shapeable text run with unique properties. /// - /// The text to create text runs from. + /// The character buffer range to create text runs from. /// The default text run properties. /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; var previousTypeface = previousProperties?.Typeface; - if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script)) { if (script == Script.Common && previousTypeface is not null) { - if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) { - return new ShapeableTextCharacters(text.Take(fallbackCount), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } if (previousTypeface is not null) { - if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) { - return new ShapeableTextCharacters(text.Take(count), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } var codepoint = Codepoint.ReplacementCodepoint; - var codepointEnumerator = new CodepointEnumerator(text.Skip(count)); + var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count)); while (codepointEnumerator.MoveNext()) { @@ -118,10 +188,10 @@ namespace Avalonia.Media.TextFormatting defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found - return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -130,7 +200,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); + var enumerator = new GraphemeEnumerator(characterBufferRange); while (enumerator.MoveNext()) { @@ -144,20 +214,20 @@ namespace Avalonia.Media.TextFormatting count += grapheme.Text.Length; } - return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel); + return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); } /// /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// The text. + /// The character buffer range to shape. /// The typeface that is used to find matching characters. /// /// The shapeable length. /// /// - protected static bool TryGetShapeableLength( - ReadOnlySlice text, + internal static bool TryGetShapeableLength( + CharacterBufferRange characterBufferRange, Typeface typeface, Typeface? defaultTypeface, out int length, @@ -166,7 +236,7 @@ namespace Avalonia.Media.TextFormatting length = 0; script = Script.Unknown; - if (text.Length == 0) + if (characterBufferRange.Length == 0) { return false; } @@ -174,7 +244,7 @@ namespace Avalonia.Media.TextFormatting var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); + var enumerator = new GraphemeEnumerator(characterBufferRange); while (enumerator.MoveNext()) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 5a2169630b..a1b8985b43 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextCharacters shapedRun: - { - currentWidth += shapedRun.Size.Width; - - if (currentWidth > availableWidth) { - if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + currentWidth += shapedRun.Size.Width; + + if (currentWidth > availableWidth) { - if (isWordEllipsis && measuredLength < textLine.Length) + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { - var currentBreakPosition = 0; + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; - var lineBreaker = new LineBreakEnumerator(currentRun.Text); + var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; + var lineBreaker = new LineBreakEnumerator(text); - if (nextBreakPosition == 0) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; - } + var nextBreakPosition = lineBreaker.Current.PositionMeasure; - if (nextBreakPosition >= measuredLength) - { - break; + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } - } - collapsedLength += measuredLength; + collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); - collapsedRuns.AddRange(splitResult.First); - } + collapsedRuns.AddRange(splitResult.First); + } - collapsedRuns.Add(shapedSymbol); + collapsedRuns.Add(shapedSymbol); - return collapsedRuns; - } + return collapsedRuns; + } - availableWidth -= currentRun.Size.Width; + availableWidth -= currentRun.Size.Width; - - break; - } + + break; + } case { } drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var collapsedRuns = new List(textRuns.Count); - collapsedRuns.AddRange(splitResult.First); - } + if (collapsedLength > 0) + { + var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + + collapsedRuns.AddRange(splitResult.First); + } + + collapsedRuns.Add(shapedSymbol); - collapsedRuns.Add(shapedSymbol); + return collapsedRuns; + } - return collapsedRuns; + break; } - - break; - } } - collapsedLength += currentRun.TextSourceLength; + collapsedLength += currentRun.Length; runIndex++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs index 21e354a119..ffb879e721 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs @@ -7,9 +7,9 @@ { public TextEndOfLine(int textSourceLength = DefaultTextSourceLength) { - TextSourceLength = textSourceLength; + Length = textSourceLength; } - public override int TextSourceLength { get; } + public override int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7bad95c4a2..93eb4811b9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[i]; - if (currentLength + currentRun.TextSourceLength < length) + if (currentLength + currentRun.Length < length) { - currentLength += currentRun.TextSourceLength; + currentLength += currentRun.Length; continue; } - var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i; + var firstCount = currentRun.Length >= 1 ? i + 1 : i; var first = new List(firstCount); @@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.TextSourceLength == length) + if (currentLength + currentRun.Length == length) { var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { - var offset = currentRun.TextSourceLength >= 1 ? 1 : 0; + var offset = currentRun.Length >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in textRuns) { - if (textRun.Text.IsEmpty) + if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0) { - var text = new char[textRun.TextSourceLength]; + var characterBuffer = new CharacterBufferReference(new char[textRun.Length]); - biDiData.Append(text); + biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length)); } else { - biDiData.Append(textRun.Text); + var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); + + biDiData.Append(text); } } @@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting case ShapeableTextCharacters shapeableRun: { var groupedRuns = new List(2) { shapeableRun }; - var text = currentRun.Text; - var start = currentRun.Text.Start; - var length = currentRun.Text.Length; - var bufferOffset = currentRun.Text.BufferOffset; + var characterBufferReference = currentRun.CharacterBufferReference; + var length = currentRun.Length; + var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; while (index + 1 < processedRuns.Count) { @@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting { groupedRuns.Add(nextRun); - length += nextRun.Text.Length; - - if (start > nextRun.Text.Start) - { - start = nextRun.Text.Start; - } + length += nextRun.Length; - if (bufferOffset > nextRun.Text.BufferOffset) + if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar) { - bufferOffset = nextRun.Text.BufferOffset; + offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar; } - text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset); + characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter); index++; @@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); break; } @@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting } private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options) + IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) { var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; - var splitResult = shapedBuffer.Split(currentRun.Text.Length); + var splitResult = shapedBuffer.Split(currentRun.Length); shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); @@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; - var runText = ReadOnlySlice.Empty; + CharacterBufferRange runText = default; for (var i = 0; i < textCharacters.Count; i++) { @@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting yield return new[] { drawableRun }; - levelIndex += drawableRun.TextSourceLength; + levelIndex += drawableRun.Length; continue; } - runText = currentRun.Text; + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); for (; j < runText.Length;) { @@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting { endOfLine = textEndOfLine; - textSourceLength += textEndOfLine.TextSourceLength; + textSourceLength += textEndOfLine.Length; textRuns.Add(textRun); @@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting { if (TryGetLineBreak(textCharacters, out var runLineBreak)) { - var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap), + var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap, textCharacters.Properties); textRuns.Add(splitResult); @@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting } } - textSourceLength += textRun.TextSourceLength; + textSourceLength += textRun.Length; } return textRuns; @@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting { lineBreak = default; - if (textRun.Text.IsEmpty) + if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty) { return false; } - var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); + var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length); + + var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange); while (lineBreakEnumerator.MoveNext()) { @@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting lineBreak = lineBreakEnumerator.Current; - return lineBreak.PositionWrap >= textRun.Text.Length || true; + return lineBreak.PositionWrap >= textRun.Length || true; } return false; @@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting { if(shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0]; + var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) @@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting currentWidth += glyphInfo.GlyphAdvance; } - measuredLength += currentRun.TextSourceLength; + measuredLength += currentRun.Length; } break; @@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting goto found; } - measuredLength += currentRun.TextSourceLength; + measuredLength += currentRun.Length; currentWidth += currentRun.Size.Width; break; @@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; - var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length); + var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; @@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[index]; - var lineBreaker = new LineBreakEnumerator(currentRun.Text); + var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + var lineBreaker = new LineBreakEnumerator(runText); var breakFound = false; @@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { - if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) + if (lineBreaker.Current.PositionWrap != currentRun.Length) { //We already found the next possible wrap position. breakFound = true; @@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting { currentPosition += lineBreaker.Current.PositionWrap; - if (lineBreaker.Current.PositionWrap != currentRun.Text.Length) + if (lineBreaker.Current.PositionWrap != currentRun.Length) { break; } @@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting currentRun = textRuns[index]; - lineBreaker = new LineBreakEnumerator(currentRun.Text); + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + lineBreaker = new LineBreakEnumerator(runText); } } else @@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting if (!breakFound) { - currentLength += currentRun.TextSourceLength; + currentLength += currentRun.Length; continue; } @@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting return false; } - if (Current.TextSourceLength == 0) + if (Current.Length == 0) { return false; } - _pos += Current.TextSourceLength; + _pos += Current.Length; return true; } @@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); - var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); + var characterBuffer = textRun.CharacterBufferReference; + + var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); return new ShapedTextCharacters(shapedBuffer, textRun.Properties); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index dc79e61333..f803001481 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textDecorations, flowDirection, lineHeight, letterSpacing); - _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); + _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); _textTrimming = textTrimming ?? TextTrimming.None; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 5a14eda245..2752af8f0c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting /// width in which collapsing is constrained to /// text run properties of ellipsis symbol public TextLeadingPrefixCharacterEllipsis( - ReadOnlySlice ellipsis, + string ellipsis, int prefixLength, double width, TextRunProperties textRunProperties) @@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting if (suffixCount > 0) { var splitSuffix = - endShapedRun.Split(run.TextSourceLength - suffixCount); + endShapedRun.Split(run.Length - suffixCount); collapsedRuns.Add(splitSuffix.Second!); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 96f88d1f44..3241dfd12b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting public override double Height => _textLineMetrics.Height; /// - public override int NewLineLength => _textLineMetrics.NewLineLength; + public override int NewLineLength => _textLineMetrics.NewlineLength; /// public override double OverhangAfter => 0; @@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting { var lastRun = _textRuns[_textRuns.Count - 1]; - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width); } // process hit that happens within the line @@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; while (rightToLeftIndex + 1 <= _textRuns.Count - 1) { - var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; + var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters; if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) { break; } - currentPosition += nextShaped.TextSourceLength; + currentPosition += nextShaped.Length; rightToLeftIndex++; } @@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; - currentPosition -= currentRun.TextSourceLength; + currentPosition -= currentRun.Length; continue; } - characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - - break; + return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); } } - if (currentDistance + currentRun.Size.Width < distance) + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); + + if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance) { currentDistance += currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + + currentPosition += currentRun.Length; continue; } - characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - break; } @@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting if (shapedRun.GlyphRun.IsLeftToRight) { - offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster); } - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength); break; } @@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting } else { - characterHit = new CharacterHit(currentPosition, run.TextSourceLength); + characterHit = new CharacterHit(currentPosition, run.Length); } break; } @@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting rightToLeftWidth -= currentRun.Size.Width; - if (currentPosition + currentRun.TextSourceLength >= characterIndex) + if (currentPosition + currentRun.Length >= characterIndex) { break; } - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; - remainingLength -= currentRun.TextSourceLength; + remainingLength -= currentRun.Length; i--; } @@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.Length >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting //No hit hit found so we add the full width currentDistance += currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; - remainingLength -= currentRun.TextSourceLength; + currentPosition += currentRun.Length; + remainingLength -= currentRun.Length; } } else @@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting //No hit hit found so we add the full width currentDistance -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; - remainingLength -= currentRun.TextSourceLength; + currentPosition += currentRun.Length; + remainingLength -= currentRun.Length; } } @@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting { currentGlyphRun = shapedTextCharacters.GlyphRun; - if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length) + if (currentPosition + remainingLength <= currentPosition + currentRun.Length) { - characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); + characterHit = new CharacterHit(currentPosition + remainingLength); distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); return true; } - if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) + if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit) { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { @@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting return true; } - if (characterIndex == currentPosition + currentRun.TextSourceLength) + if (characterIndex == currentPosition + currentRun.Length) { distance = currentRun.Size.Width; @@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedRun: { - characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); + nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); break; } default: { - characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); + nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); break; } } - return characterHit; + if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + { + return characterHit; + } + + return nextCharacterHit; } /// @@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var currentShapedRun = currentRun as ShapedTextCharacters; - TextRunBounds currentRunBounds; double combinedWidth; - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - - if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters currentShapedRun) { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; + var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) + if (currentPosition + currentRun.Length <= firstTextSourceIndex) { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) - { - break; - } + startX += currentRun.Size.Width; - rightToLeftIndex++; + currentPosition += currentRun.Length; - rightToLeftWidth += nextShapedRun.Size.Width; - - if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) - { - break; - } - - currentShapedRun = nextShapedRun; + continue; } - startX = startX + rightToLeftWidth; + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + double startOffset; - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + double endOffset; - var rightToLeftRunBounds = new List { currentRunBounds }; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - for (int i = rightToLeftIndex - 1; i >= index; i--) - { - currentShapedRun = TextRuns[i] as ShapedTextCharacters; + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if(currentShapedRun == null) - { - continue; - } + startX += startOffset; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + endX += endOffset; - rightToLeftRunBounds.Insert(0, currentRunBounds); + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - currentPosition += currentRunBounds.Length; + characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + + currentDirection = FlowDirection.LeftToRight; } + else + { + var rightToLeftIndex = index; + var rightToLeftWidth = currentShapedRun.Size.Width; - combinedWidth = endX - startX; + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) + { + if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + { + break; + } - currentRect = new Rect(startX, 0, combinedWidth, Height); + rightToLeftIndex++; - currentDirection = FlowDirection.RightToLeft; + rightToLeftWidth += nextShapedRun.Size.Width; - if (!MathUtilities.IsZero(combinedWidth)) - { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); - } + if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) + { + break; + } - startX = endX; - } - else - { - if (currentShapedRun != null) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + currentShapedRun = nextShapedRun; + } - currentPosition += offset; + startX += rightToLeftWidth; - var startIndex = currentRun.Text.Start + offset; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); - double startOffset; - double endOffset; + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var rightToLeftRunBounds = new List { currentRunBounds }; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - else + for (int i = rightToLeftIndex - 1; i >= index; i--) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - if (currentPosition < startIndex) - { - startOffset = endOffset; - } - else + if (TextRuns[i] is not ShapedTextCharacters) { - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + continue; } - } - startX += startOffset; + currentShapedRun = (ShapedTextCharacters)TextRuns[i]; - endX += endOffset; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + rightToLeftRunBounds.Insert(0, currentRunBounds); - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; - currentDirection = FlowDirection.LeftToRight; - } - else - { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; + currentPosition += currentRunBounds.Length; + } - currentPosition += currentRun.TextSourceLength; + combinedWidth = endX - startX; - continue; - } + currentRect = new Rect(startX, 0, combinedWidth, Height); + + currentDirection = FlowDirection.RightToLeft; - if (currentPosition < firstTextSourceIndex) + if (!MathUtilities.IsZero(combinedWidth)) { - startX += currentRun.Size.Width; + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); } - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX += currentRun.Size.Width; + startX = endX; + } + } + else + { + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; - characterLength = currentRun.TextSourceLength; - } + currentPosition += currentRun.Length; + + continue; } - if (endX < startX) + if (currentPosition < firstTextSourceIndex) { - (endX, startX) = (startX, endX); + startX += currentRun.Size.Width; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (currentPosition + currentRun.Length <= characterIndex) { - characterLength = NewLineLength; + endX += currentRun.Size.Width; + + characterLength = currentRun.Length; } + } - combinedWidth = endX - startX; + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; + } - currentPosition += characterLength; + combinedWidth = endX - startX; - remainingLength -= characterLength; + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); - startX = endX; + currentPosition += characterLength; - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) - { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + remainingLength -= characterLength; - var textBounds = result[result.Count - 1]; + startX = endX; - textBounds.Rectangle = currentRect; + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) + { + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - textBounds.TextRunBounds.Add(currentRunBounds); - } - else - { - currentRect = currentRunBounds.Rectangle; + var textBounds = result[result.Count - 1]; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); - } + textBounds.Rectangle = currentRect; + + textBounds.TextRunBounds.Add(currentRunBounds); } + else + { + currentRect = currentRunBounds.Rectangle; - lastRunBounds = currentRunBounds; + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + lastRunBounds = currentRunBounds; + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) @@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) + if (currentPosition + currentRun.Length < firstTextSourceIndex) { startX -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; continue; } @@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; - var startIndex = currentRun.Text.Start + offset; + var startIndex = currentPosition; double startOffset; double endOffset; @@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting } else { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) + if (currentPosition + currentRun.Length <= characterIndex) { endX -= currentRun.Size.Width; } @@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting { startX -= currentRun.Size.Width; - characterLength = currentRun.TextSourceLength; + characterLength = currentRun.Length; } } @@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; - var startIndex = currentRun.Text.Start + offset; + var startIndex = currentPosition; double startOffset; double endOffset; @@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting return true; } - var characterIndex = codepointIndex - shapedRun.Text.Start; + //var characterIndex = codepointIndex - shapedRun.Text.Start; - if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) - { - foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); - } + //if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) + //{ + // foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); + //} nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? foundCharacterHit : @@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting if (textPosition == currentPosition) { - nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); + nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); return true; } @@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting } } - currentPosition += currentRun.TextSourceLength; + currentPosition += currentRun.Length; runIndex++; } @@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting } default: { - if (characterIndex == currentPosition + currentRun.TextSourceLength) + if (characterIndex == currentPosition + currentRun.Length) { previousCharacterHit = new CharacterHit(currentPosition); @@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting } } - currentPosition -= currentRun.TextSourceLength; + currentPosition -= currentRun.Length; runIndex--; } @@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedRun: { + var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; + + if (firstCluster > codepointIndex) + { + break; + } + if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) { if (shapedRun.ShapedBuffer.IsLeftToRight) { - if (currentRun.Text.Start >= codepointIndex) + if (firstCluster >= codepointIndex) { return --runIndex; } } else { - if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length) + if (codepointIndex > firstCluster + currentRun.Length) { return --runIndex; } @@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting if (direction == LogicalDirection.Forward) { - if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End) + if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) { return runIndex; } } else { - if (codepointIndex > currentRun.Text.Start && - codepointIndex <= currentRun.Text.Start + currentRun.Text.Length) + if (codepointIndex > firstCluster && + codepointIndex <= firstCluster + currentRun.Length) { return runIndex; } @@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting return runIndex; } + textPosition += currentRun.Length; + break; } @@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting return runIndex; } + textPosition += currentRun.Length; + break; } } runIndex++; previousRun = currentRun; - textPosition += currentRun.TextSourceLength; } return runIndex; @@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting case ShapedTextCharacters textRun: { var textMetrics = - new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize); + new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) { @@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewlineLength; + newLineLength = textRun.GlyphRun.Metrics.NewLineLength; } widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index 1799c9d3db..40a7f6167a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -6,13 +6,13 @@ /// public readonly struct TextLineMetrics { - public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline, + public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline, int trailingWhitespaceLength, double width, double widthIncludingTrailingWhitespace) { HasOverflowed = hasOverflowed; Height = height; - NewLineLength = newLineLength; + NewlineLength = newlineLength; Start = start; TextBaseline = textBaseline; TrailingWhitespaceLength = trailingWhitespaceLength; @@ -33,7 +33,7 @@ /// /// Gets the number of newline characters at the end of a line. /// - public int NewLineLength { get; } + public int NewlineLength { get; } /// /// Gets the distance from the start of a paragraph to the starting point of a line. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index 0382e66b5a..dc21c9b6f2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -5,9 +5,9 @@ /// public readonly struct TextMetrics { - public TextMetrics(Typeface typeface, double fontRenderingEmSize) + public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize) { - var fontMetrics = typeface.GlyphTypeface.Metrics; + var fontMetrics = glyphTypeface.Metrics; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 26c3f8947a..0306054767 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text source length. /// - public virtual int TextSourceLength => DefaultTextSourceLength; + public virtual int Length => DefaultTextSourceLength; /// /// Gets the text run's text. /// - public virtual ReadOnlySlice Text => default; + public virtual CharacterBufferReference CharacterBufferReference => default; /// /// A set of properties shared by every characters in the run @@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting { unsafe { - fixed (char* charsPtr = _textRun.Text.Span) + var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer; + + fixed (char* charsPtr = characterBuffer.Span) { - return new string(charsPtr, 0, _textRun.Text.Length); + return new string(charsPtr, 0, characterBuffer.Span.Length); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index 615b1553b6..4aacec7c48 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs @@ -1,7 +1,5 @@ using System; -using System.Globalization; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting } /// - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default) { - return _platformImpl.ShapeText(text, options); + return _platformImpl.ShapeText(text, length, options); + } + + public ShapedBuffer ShapeText(string text, TextShaperOptions options = default) + { + return ShapeText(new CharacterBufferReference(text), text.Length, options); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 670d94e928..1de04ad061 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties) + public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index dbffbdf060..7c94715aa4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. public TextTrailingWordEllipsis( - ReadOnlySlice ellipsis, + string ellipsis, double width, TextRunProperties textRunProperties ) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 72df2815a9..0c51b0898d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/ +using System; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Appends text to the bidi data. /// /// The text to process. - public void Append(ReadOnlySlice text) + public void Append(CharacterBufferRange 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 de40839853..9e5186552e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,6 +1,5 @@ -using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { @@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// - public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) + public static Codepoint ReadAt(IReadOnlyList text, int index, out int count) { count = 1; - if (index >= text.Length) + if (index >= text.Count) { return ReplacementCodepoint; } @@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { hi = code; - if (index + 1 == text.Length) + if (index + 1 == text.Count) { return ReplacementCodepoint; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9e1f748ebb..330ead476a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -1,12 +1,13 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySlice _text; + private CharacterBufferRange _text; + private int _pos; - public CodepointEnumerator(ReadOnlySlice text) + public CodepointEnumerator(CharacterBufferRange text) { _text = text; Current = Codepoint.ReplacementCodepoint; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f268340eb9..69015fb17d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,13 +1,13 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// - public readonly struct Grapheme + public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySlice text) + public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) { FirstCodepoint = firstCodepoint; Text = text; @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The text that is representing the . /// - public ReadOnlySlice Text { 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 1e4ac8fe0f..5ca120c856 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.Runtime.InteropServices; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private ReadOnlySlice _text; + private CharacterBufferRange _text; - public GraphemeEnumerator(ReadOnlySlice text) + public GraphemeEnumerator(CharacterBufferRange text) { _text = text; Current = default; @@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var text = _text.Take(processor.CurrentCodeUnitOffset); - Current = new Grapheme(firstCodepoint, text); + Current = new Grapheme(firstCodepoint, text.Span); _text = _text.Skip(processor.CurrentCodeUnitOffset); @@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode [StructLayout(LayoutKind.Auto)] private ref struct Processor { - private readonly ReadOnlySlice _buffer; + private readonly CharacterBufferRange _buffer; private int _codeUnitLengthOfCurrentScalar; - internal Processor(ReadOnlySlice buffer) + internal Processor(CharacterBufferRange 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 e12a7c06f1..41a476c17e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -2,7 +2,8 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/ -using Avalonia.Utilities; +using System; +using System.Collections.Generic; namespace Avalonia.Media.TextFormatting.Unicode { @@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public ref struct LineBreakEnumerator { - private readonly ReadOnlySlice _text; + private readonly IReadOnlyList _text; private int _position; private int _lastPosition; private LineBreakClass _currentClass; @@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private int _lb30a; private bool _lb31; - public LineBreakEnumerator(ReadOnlySlice text) + public LineBreakEnumerator(IReadOnlyList text) : this() { _text = text; @@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30a = 0; } - while (_position < _text.Length) + while (_position < _text.Count) { _lastPosition = _position; var lastClass = _nextClass; @@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - if (_position >= _text.Length) + if (_position >= _text.Count) { - if (_lastPosition < _text.Length) + if (_lastPosition < _text.Count) { - _lastPosition = _text.Length; + _lastPosition = _text.Count; var required = false; diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 19ca1a0198..7ba25eb005 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -1,21 +1,16 @@ using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; namespace Avalonia.Media { public sealed class TextLeadingPrefixTrimming : TextTrimming { - private readonly ReadOnlySlice _ellipsis; + private readonly string _ellipsis; private readonly int _prefixLength; - public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength) - { - } - - public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength) + public TextLeadingPrefixTrimming(string ellipsis, int prefixLength) { _prefixLength = prefixLength; - _ellipsis = new ReadOnlySlice(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 5bb35f0ba7..2edbaabbc6 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -1,21 +1,16 @@ using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; namespace Avalonia.Media { public sealed class TextTrailingTrimming : TextTrimming { - private readonly ReadOnlySlice _ellipsis; + private readonly string _ellipsis; private readonly bool _isWordBased; - - public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased) - { - } - public TextTrailingTrimming(char[] ellipsis, bool isWordBased) + public TextTrailingTrimming(string ellipsis, bool isWordBased) { _isWordBased = isWordBased; - _ellipsis = new ReadOnlySlice(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs index e2737210be..34642c11df 100644 --- a/src/Avalonia.Base/Media/TextTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrimming.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// public abstract class TextTrimming { - internal const char DefaultEllipsisChar = '\u2026'; + internal const string DefaultEllipsisChar = "\u2026"; /// /// Text is not trimmed. diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index 10e58b7d0b..ff91097eda 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -1,6 +1,5 @@ using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Platform { @@ -13,9 +12,9 @@ namespace Avalonia.Platform /// /// Shapes the specified region within the text and returns a shaped buffer. /// - /// The text. + /// The text buffer. /// Text shaper options to customize the shaping process. /// A shaped glyph run. - ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options); + ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 2019ad6faa..18cb7a6308 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -31,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 482f807fe0..39c0cd5556 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -111,14 +111,6 @@ namespace Avalonia.Utilities } } - /// - /// Defines an implicit conversion of a to a - /// - public static implicit operator ReadOnlySlice(ArraySlice slice) - { - return new ReadOnlySlice(slice._data, 0, slice.Length, slice.Start); - } - /// /// Defines an implicit conversion of an array to a /// diff --git a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs deleted file mode 100644 index 583a3139b9..0000000000 --- a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. - /// - /// The type of elements in the slice. - [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct - { - private readonly int _bufferOffset; - - /// - /// Gets an empty - /// - public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty()); - - private readonly ReadOnlyMemory _buffer; - - public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int bufferOffset = 0) - { -#if DEBUG - if (start.CompareTo(0) < 0) - { - throw new ArgumentOutOfRangeException(nameof (start)); - } - - if (length.CompareTo(buffer.Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (length)); - } -#endif - - _buffer = buffer; - Start = start; - Length = length; - _bufferOffset = bufferOffset; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets a value that indicates whether this instance of is Empty. - /// - public bool IsEmpty => Length == 0; - - /// - /// Get the underlying span. - /// - public ReadOnlySpan Span => _buffer.Span.Slice(_bufferOffset, Length); - - /// - /// Get the buffer offset. - /// - public int BufferOffset => _bufferOffset; - - /// - /// Get the underlying buffer. - /// - public ReadOnlyMemory Buffer => _buffer; - - /// - /// Returns a value to specified element of the slice. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public 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 - return Span[index]; - } - } - - /// - /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. - /// - /// The start of the sub slice. - /// The length of the sub slice. - /// A that contains the specified number of elements from the specified start. - public ReadOnlySlice AsSlice(int start, int length) - { - if (IsEmpty) - { - return this; - } - - if (length == 0) - { - return Empty; - } - - if (start < 0 || _bufferOffset + start > _buffer.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(start)); - } - - if (_bufferOffset + start + length > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, start, length, _bufferOffset); - } - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public ReadOnlySlice Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start, length, _bufferOffset); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public ReadOnlySlice Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start + length, Length - length, _bufferOffset + length); - } - - /// - /// Returns an enumerator for the slice. - /// - public ImmutableReadOnlyListStructEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - int IReadOnlyCollection.Count => Length; - - T IReadOnlyList.this[int index] => this[index]; - - public static implicit operator ReadOnlySlice(T[] array) - { - return new ReadOnlySlice(array); - } - - public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) - { - return new ReadOnlySlice(memory); - } - - public static implicit operator ReadOnlySpan(ReadOnlySlice slice) => slice.Span; - - internal class ReadOnlySliceDebugView - { - private readonly ReadOnlySlice _readOnlySlice; - - public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) - { - _readOnlySlice = readOnlySlice; - } - - public int Start => _readOnlySlice.Start; - - public int End => _readOnlySlice.End; - - public int Length => _readOnlySlice.Length; - - public bool IsEmpty => _readOnlySlice.IsEmpty; - - public ReadOnlySpan Items => _readOnlySlice.Span; - } - } -} diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 108a38d86b..ee31b85be9 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -4,7 +4,7 @@ using System.Text; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -namespace Avalonia.Controls.Documents +namespace Avalonia.Controls.Documents { /// /// LineBreak element that forces a line breaking. @@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = Environment.NewLine.AsMemory(); + var text = Environment.NewLine; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 2bd66b8a64..5d7b8998e6 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = (Text ?? "").AsMemory(); + var text = Text ?? ""; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c8e05e5cb3..08156ae00f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -630,7 +630,7 @@ namespace Avalonia.Controls } else { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + textSource = new SimpleTextSource(text ?? "", defaultProperties); } return new TextLayout( @@ -829,12 +829,12 @@ namespace Avalonia.Controls protected readonly struct SimpleTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly TextRunProperties _defaultProperties; - public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + public SimpleTextSource(string text, TextRunProperties defaultProperties) { - _text = text; + _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); _defaultProperties = defaultProperties; } @@ -852,7 +852,7 @@ namespace Avalonia.Controls return new TextEndOfParagraph(); } - return new TextCharacters(runText, _defaultProperties); + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); } } @@ -873,21 +873,28 @@ namespace Avalonia.Controls foreach (var textRun in _textRuns) { - if (textRun.TextSourceLength == 0) + if (textRun.Length == 0) { continue; } - if (textSourceIndex >= currentPosition + textRun.TextSourceLength) + if (textSourceIndex >= currentPosition + textRun.Length) { - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; continue; } - if (textRun is TextCharacters) + if (textRun is TextCharacters) { - return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!); + 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 textRun; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 699170df5a..1bdec878d9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -961,7 +961,9 @@ namespace Avalonia.Controls var length = 0; - var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); + var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); + + var graphemeEnumerator = new GraphemeEnumerator(inputRange); while (graphemeEnumerator.MoveNext()) { diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 6a79760011..52815b943d 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -77,12 +77,14 @@ namespace Avalonia.Controls foreach (var run in textLine.TextRuns) { - if(run.Text.Length > 0) + if(run.Length > 0) { + var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length); + #if NET6_0 - builder.Append(run.Text.Span); + builder.Append(characterBufferRange.Span); #else - builder.Append(run.Text.Span.ToArray()); + builder.Append(characterBufferRange.Span.ToArray()); #endif } } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 688b8e0398..1cc0fa73bb 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -145,13 +145,15 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + + return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index eaf588c27d..98eb35d5c5 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; 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; @@ -12,8 +11,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { + var text = new CharacterBufferRange(characterBufferReference, length); var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -21,21 +21,21 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, 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 = ((GlyphTypefaceImpl)typeface).Font; font.Shape(buffer); - if(buffer.Direction == Direction.RightToLeft) + if (buffer.Direction == Direction.RightToLeft) { buffer.Reverse(); } @@ -64,12 +64,12 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if(text.Buffer.Span[glyphCluster] == '\t') + if (text[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } @@ -87,7 +87,7 @@ namespace Avalonia.Skia var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint(second.Codepoint).IsBreakChar) @@ -98,7 +98,7 @@ namespace Avalonia.Skia 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; @@ -109,7 +109,7 @@ namespace Avalonia.Skia { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 7f2cbc6182..6685dd00b9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; 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; @@ -12,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -21,7 +20,7 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); MergeBreakPair(buffer); @@ -46,7 +45,9 @@ namespace Avalonia.Direct2D1.Media var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(characterBufferReference, length); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -64,7 +65,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (text.Buffer.Span[glyphCluster] == '\t') + if (characterBufferRange[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index df16c1b34f..363fb3f5b3 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -181,9 +181,7 @@ namespace Avalonia.Base.UnitTests.Media var count = glyphAdvances.Length; var glyphIndices = new ushort[count]; - var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1]; - - var characters = new ReadOnlySlice(Enumerable.Repeat('a', count).ToArray(), start, count); + var characters = Enumerable.Repeat('a', count).ToArray(); return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, glyphClusters: glyphClusters, biDiLevel: bidiLevel); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index d2cea45ce1..b2c40f4ff1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -36,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); // Append - bidiData.Append(text.AsMemory()); + bidiData.Append(new CharacterBufferRange(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 c9f869cea9..4e0207a85d 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Visuals.UnitTests.Media.TextFormatting; using Xunit; @@ -37,11 +38,11 @@ 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(text.AsMemory()); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); enumerator.MoveNext(); - var actual = enumerator.Current.Text.Span; + var actual = enumerator.Current.Text; var pass = true; @@ -86,9 +87,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var textMemory = text.AsMemory(); - - var enumerator = new GraphemeEnumerator(textMemory); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); var count = 0; diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index 15be1200c8..b2648bf348 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void BasicLatinTest() { - var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test.")); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); @@ -55,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTextWithOuterWhitespace() { - var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas ")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(1, positionsF[0].PositionWrap); Assert.Equal(0, positionsF[0].PositionMeasure); @@ -82,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTest() { - var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(7, positionsF[0].PositionWrap); @@ -99,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var foundBreaks = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs deleted file mode 100644 index da30ee9d02..0000000000 --- a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Avalonia.Utilities; -using Xunit; - -namespace Avalonia.Base.UnitTests.Utilities -{ - public class ReadOnlySpanTests - { - [Fact] - public void Should_Skip() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var skipped = slice.Skip(2); - - var expected = buffer.Skip(2); - - Assert.Equal(expected, skipped); - } - - [Fact] - public void Should_Take() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var taken = slice.Take(8); - - var expected = buffer.Take(8); - - Assert.Equal(expected, taken); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 8cc8e4c16f..8e06fbd831 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.Text.Span.ToString())); + target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString())); Assert.Equal("****", actual); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 3b9caa393e..4083a67b5e 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(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -39,8 +39,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetNextCaretCharacterHit(characterHit); @@ -62,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -84,8 +82,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit); @@ -107,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -116,16 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media var characterHit = glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - - var characterHit = + var characterHit = glyphRun.GetCharacterHitFromDistance(0, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } var rects = BuildRects(glyphRun); @@ -218,15 +212,22 @@ namespace Avalonia.Skia.UnitTests.Media private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer) { - return new GlyphRun( + var glyphRun = new GlyphRun( shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, - shapedBuffer.Text, + shapedBuffer.CharacterBufferRange, shapedBuffer.GlyphIndices, shapedBuffer.GlyphAdvances, shapedBuffer.GlyphOffsets, shapedBuffer.GlyphClusters, shapedBuffer.BidiLevel); + + if(shapedBuffer.BidiLevel == 1) + { + shapedBuffer.GlyphInfos.Span.Reverse(); + } + + return glyphRun; } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index 005bcdf70e..aa499bb135 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -29,8 +29,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var runText = _runTexts[index]; - return new TextCharacters( - new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); + return new TextCharacters(runText, _defaultStyle); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index dee4fe7f77..f12f42bd5e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -1,30 +1,33 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { internal class SingleBufferTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) { - _text = text.AsMemory(); + _text = new CharacterBufferRange(text); _defaultGenericPropertiesRunProperties = defaultProperties; } public TextRun GetTextRun(int textSourceIndex) { - if (textSourceIndex > _text.Length) + if (textSourceIndex >= _text.Length) { return null; } - + var runText = _text.Skip(textSourceIndex); - return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties); + if (runText.IsEmpty) + { + return null; + } + + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 316926b00c..33d4fba5f1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); - Assert.Equal(text.Length, textRun.Text.Length); + Assert.Equal(text.Length, textRun.Length); } } @@ -82,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ValueSpan(9, 1, defaultProperties) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns); + var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); var formatter = new TextFormatterImpl(); @@ -97,7 +97,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[i]; - Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); + Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length); } } } @@ -166,7 +166,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = textLine.TextRuns[0]; - Assert.Equal(4, firstRun.Text.Length); + Assert.Equal(4, firstRun.Length); } } @@ -216,7 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var expected = new List(); @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans); + var textSource = new FormattedTextSource(text, defaultProperties, styleSpans); var formatter = new TextFormatterImpl(); @@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties)); } currentHeight += textLine.Height; @@ -472,7 +472,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak); - Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength)); + Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length)); textPosition += textLine.Length; @@ -534,7 +534,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans); + var textSource = new FormattedTextSource(text, defaultProperties, spans); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); @@ -614,8 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green); } - return new TextCharacters(_text.AsMemory(), - new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); + return new TextCharacters(_text, 0, _text.Length, 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 d6da2c77c4..a407b38eb1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -60,9 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("1 ", actual); @@ -144,8 +144,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(text.AsMemory()); - var inner = new GraphemeEnumerator(text.AsMemory()); + var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); + var inner = new GraphemeEnumerator(new CharacterBufferRange(text)); var i = 0; var j = 0; @@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting break; } - inner = new GraphemeEnumerator(text.AsMemory()); + inner = new GraphemeEnumerator(new CharacterBufferRange(text)); i += outer.Current.Text.Length; } @@ -223,10 +223,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = SingleLineText.Substring(textRun.Text.Start, - textRun.Text.Length); + var actual = SingleLineText[..textRun.Length]; Assert.Equal("01", actual); @@ -260,9 +259,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("89", actual); @@ -296,7 +295,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(1, textRun.Text.Length); + Assert.Equal(1, textRun.Length); Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } @@ -330,9 +329,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("😄", actual); @@ -369,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( MultiLineText.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -402,7 +401,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( text.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -558,7 +557,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; - Assert.Equal(7, textRun.Text.Length); + Assert.Equal(7, textRun.Length); var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); @@ -668,10 +667,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(5, layout.TextLines.Count); - Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text); - Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text); + 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])); } } @@ -815,7 +814,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { Assert.True(textLine.Width <= maxWidth); - var actual = new string(textLine.TextRuns.Cast().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray()); + var actual = new string(textLine.TextRuns.Cast() + .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) + .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); + var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); @@ -966,7 +968,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var i = 0; - var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory()); + var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); while (graphemeEnumerator.MoveNext()) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 87de9ed11f..d6257a0de8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -187,14 +187,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) - .ToArray(); + var clusters = BuildGlyphClusters(textLine); var nextCharacterHit = new CharacterHit(0); - for (var i = 0; i < clusters.Length; i++) + for (var i = 0; i < clusters.Count; i++) { - Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex); + var expectedCluster = clusters[i]; + var actualCluster = nextCharacterHit.FirstCharacterIndex; + + Assert.Equal(expectedCluster, actualCluster); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } @@ -406,7 +408,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(collapsedLine.HasCollapsed); - var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray(); + var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); @@ -450,8 +452,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentHit = textLine.GetNextCaretCharacterHit(currentHit); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(1, currentHit.TrailingLength); + Assert.Equal(4, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); } } @@ -473,18 +475,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(2, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(2, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(1, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(1, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(0, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); @@ -509,13 +511,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var characterHit = textLine.GetCharacterHitFromDistance(50); - Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(5, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetCharacterHitFromDistance(32); - Assert.Equal(2, characterHit.FirstCharacterIndex); - Assert.Equal(1, characterHit.TrailingLength); + Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(0, characterHit.TrailingLength); } } @@ -649,7 +651,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var run = textRuns[i]; var bounds = runBounds[i]; - Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width); } @@ -683,13 +685,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new TextCharacters(new ReadOnlySlice("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default)); case 10: - return new TextCharacters(new ReadOnlySlice("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default)); case 20: - return new TextCharacters(new ReadOnlySlice("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default)); case 30: - return new TextCharacters(new ReadOnlySlice("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -698,7 +700,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting private class DrawableRunTextSource : ITextSource { - const string Text = "_A_A"; + private const string Text = "_A_A"; public TextRun GetTextRun(int textSourceIndex) { @@ -707,11 +709,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting case 0: return new CustomDrawableRun(); case 1: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); - case 2: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); + case 5: return new CustomDrawableRun(); - case 3: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); + case 6: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -815,19 +817,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); - var text = "0123".AsMemory(); + var text = "0123"; var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties); + var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties), + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties) + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -838,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + var textBounds = textLine.GetTextBounds(0, textLine.Length); Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); @@ -848,17 +850,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(14, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); + textBounds = textLine.GetTextBounds(1, firstRun.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); @@ -878,7 +880,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 3); @@ -899,11 +901,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); - Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); + Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); textBounds = textLine.GetTextBounds(0, text.Length); @@ -925,7 +927,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 4); @@ -941,13 +943,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); - Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length)); + Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length)); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 5); Assert.Equal(2, textBounds.Count); - Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length))); + Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); @@ -960,7 +962,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } - } + } private class FixedRunsTextSource : ITextSource { @@ -982,7 +984,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return textRun; } - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; } return null; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 94933e334d..63e0083b1d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -14,11 +14,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\n\r\n".AsMemory(); + var text = "\n\r\n"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture); var shapedBuffer = TextShaper.Current.ShapeText(text, options); - Assert.Equal(shapedBuffer.Text.Length, text.Length); + Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length); Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(1, shapedBuffer.GlyphClusters[1]); @@ -31,7 +31,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\t".AsMemory(); + var text = "\t"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100); var shapedBuffer = TextShaper.Current.ShapeText(text, options); diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 7b7488bd5a..ae7e00aca1 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.UnitTests { public class HarfBuzzTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength); MergeBreakPair(buffer); @@ -45,7 +45,9 @@ namespace Avalonia.UnitTests var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, textLength); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 7c34bd192e..00bcef295a 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,24 +1,24 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); for (var i = 0; i < shapedBuffer.Length;) { - var glyphCluster = i + text.Start; - var codepoint = Codepoint.ReadAt(text, i, out var count); + var glyphCluster = i + text.OffsetToFirstChar; + + var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count); var glyphIndex = typeface.GetGlyph(codepoint); From d5bea7b6ba618fde9107125da0a73e82a246ec97 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 9 Dec 2022 12:01:47 +0100 Subject: [PATCH 2/2] Visibility cleanup --- .../TextFormatting/CharacterBufferRange.cs | 15 ---- .../CharacterBufferReference.cs | 83 +++---------------- .../Media/TextFormatting/TextCharacters.cs | 15 ---- 3 files changed, 11 insertions(+), 102 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index 045f336700..d76f212f26 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -47,21 +47,6 @@ namespace Avalonia.Media.TextFormatting ) { } - /// - /// Construct from unsafe character string - /// - /// pointer to character string - /// character length - public unsafe CharacterBufferRange( - char* unsafeCharacterString, - int characterLength - ) - : this( - new CharacterBufferReference(unsafeCharacterString, characterLength), - characterLength - ) - { } - /// /// Construct a from /// diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs index a15562cb52..672fcf3377 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs @@ -1,6 +1,4 @@ using System; -using System.Buffers; -using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting { @@ -26,15 +24,6 @@ namespace Avalonia.Media.TextFormatting public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) : this(characterString.AsMemory(), offsetToFirstChar) { } - - /// - /// Construct character buffer reference from unsafe character string - /// - /// pointer to character string - /// character length of unsafe string - public unsafe CharacterBufferReference(char* unsafeCharacterString, int characterLength) - : this(new UnmanagedMemoryManager(unsafeCharacterString, characterLength).Memory, 0) - { } /// /// Construct character buffer reference from memory buffer @@ -58,6 +47,17 @@ namespace Avalonia.Media.TextFormatting 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 /// @@ -110,67 +110,6 @@ namespace Avalonia.Media.TextFormatting { return !(left == right); } - - public ReadOnlyMemory CharacterBuffer { get; } - - public int OffsetToFirstChar { get; } - - /// - /// A MemoryManager over a raw pointer - /// - /// The pointer is assumed to be fully unmanaged, or externally pinned - no attempt will be made to pin this data - public sealed unsafe class UnmanagedMemoryManager : MemoryManager - where T : unmanaged - { - private readonly T* _pointer; - private readonly int _length; - - /// - /// Create a new UnmanagedMemoryManager instance at the given pointer and size - /// - /// It is assumed that the span provided is already unmanaged or externally pinned - public UnmanagedMemoryManager(Span span) - { - fixed (T* ptr = &MemoryMarshal.GetReference(span)) - { - _pointer = ptr; - _length = span.Length; - } - } - /// - /// Create a new UnmanagedMemoryManager instance at the given pointer and size - /// - public UnmanagedMemoryManager(T* pointer, int length) - { - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - _pointer = pointer; - _length = length; - } - /// - /// Obtains a span that represents the region - /// - public override Span GetSpan() => new Span(_pointer, _length); - - /// - /// Provides access to a pointer that represents the data (note: no actual pin occurs) - /// - public override MemoryHandle Pin(int elementIndex = 0) - { - if (elementIndex < 0 || elementIndex >= _length) - throw new ArgumentOutOfRangeException(nameof(elementIndex)); - return new MemoryHandle(_pointer + elementIndex); - } - /// - /// Has no effect - /// - public override void Unpin() { } - - /// - /// Releases all resources associated with this object - /// - protected override void Dispose(bool disposing) { } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 9587786c5b..0be753bd04 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -57,21 +57,6 @@ namespace Avalonia.Media.TextFormatting ) { } - /// - /// Construct a run for text content from unsafe character string - /// - public unsafe TextCharacters( - char* unsafeCharacterString, - int length, - TextRunProperties textRunProperties - ) : - this( - new CharacterBufferReference(unsafeCharacterString, length), - length, - textRunProperties - ) - { } - /// /// Internal constructor of TextContent ///