diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index f5d39e4371..499026e8b3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting throw new ArgumentOutOfRangeException(nameof(index)); } #endif - return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; + return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; } } @@ -157,8 +157,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets a span from the character buffer range /// - public ReadOnlySpan Span => - CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); /// /// Gets the character memory buffer @@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting /// /// Indicate whether the character buffer range is empty /// - internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; + internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; internal CharacterBufferRange Take(int length) { @@ -208,9 +207,7 @@ namespace Avalonia.Media.TextFormatting return new CharacterBufferRange(new CharacterBufferReference(), 0); } - var characterBufferReference = new CharacterBufferReference( - CharacterBufferReference.CharacterBuffer, - CharacterBufferReference.OffsetToFirstChar + length); + var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); return new CharacterBufferRange(characterBufferReference, Length - length); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index f677617b14..01804e1ce3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract List? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 086ea85d97..9c201bda22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { - if (textLine.TextRuns is not List textRuns || textRuns.Count == 0) + var textRuns = textLine.TextRuns; + + if (textRuns == null || textRuns.Count == 0) { return null; } @@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return new List(0); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); if (collapsedLength > 0) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } @@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting return collapsedRuns; } - availableWidth -= currentRun.Size.Width; - + availableWidth -= shapedRun.Size.Width; break; } - case { } drawableRun: + case DrawableTextRun drawableRun: { //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); if (collapsedLength > 0) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } @@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting return collapsedRuns; } + availableWidth -= drawableRun.Size.Width; + break; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index ef2abdfea0..989bf7749d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting var textWrapping = paragraphProperties.TextWrapping; FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - List drawableTextRuns; + IReadOnlyList textRuns; - var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); if (previousLineBreak?.RemainingRuns != null) { resolvedFlowDirection = previousLineBreak.FlowDirection; - drawableTextRuns = previousLineBreak.RemainingRuns.ToList(); + textRuns = previousLineBreak.RemainingRuns; nextLineBreak = previousLineBreak; } else { - drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection); + textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); if (nextLineBreak == null && textEndOfLine != null) { @@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength, + textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); break; } @@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting /// The text run's. /// The length to split at. /// The split text runs. - internal static SplitResult> SplitDrawableRuns(List textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) { var currentLength = 0; @@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.Length >= 1 ? i + 1 : i; - var first = new List(firstCount); + var first = new List(firstCount); if (firstCount > 1) { @@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { @@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = new List(secondCount); if (currentRun is ShapedTextRun shapedTextCharacters) { @@ -131,18 +130,18 @@ namespace Avalonia.Media.TextFormatting first.Add(split.First); second.Add(split.Second!); - } + } for (var j = 1; j < secondCount; j++) { second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + return new SplitResult>(textRuns, null); } /// @@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var drawableTextRuns = new List(); + var shapedRuns = new List(); var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) @@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case DrawableTextRun drawableRun: - { - drawableTextRuns.Add(drawableRun); - - break; - } - case UnshapedTextRun shapeableRun: { var groupedRuns = new List(2) { shapeableRun }; @@ -245,17 +237,23 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, + shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + + break; + } + default: + { + shapedRuns.Add(currentRun); break; } } } - return drawableTextRuns; + return shapedRuns; } private static IReadOnlyList ShapeTogether( @@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting if (textRun == null) { + textRuns.Add(new TextEndOfParagraph()); + + textSourceLength += TextRun.DefaultTextSourceLength; + break; } @@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting return false; } - private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) + private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) { measuredLength = 0; var currentWidth = 0.0; @@ -476,7 +478,7 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun shapedTextCharacters: { - if(shapedTextCharacters.ShapedBuffer.Length > 0) + if (shapedTextCharacters.ShapedBuffer.Length > 0) { var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; @@ -497,12 +499,12 @@ namespace Avalonia.Media.TextFormatting } measuredLength += currentRun.Length; - } + } break; } - case { } drawableTextRun: + case DrawableTextRun drawableTextRun: { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { @@ -510,14 +512,20 @@ namespace Avalonia.Media.TextFormatting } measuredLength += currentRun.Length; - currentWidth += currentRun.Size.Width; + currentWidth += drawableTextRun.Size.Width; + + break; + } + default: + { + measuredLength += currentRun.Length; break; } } } - found: + found: return measuredLength != 0; } @@ -553,13 +561,13 @@ namespace Avalonia.Media.TextFormatting /// /// The current line break if the line was explicitly broken. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex, + private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak) { - if(textRuns.Count == 0) + if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -575,46 +583,24 @@ namespace Avalonia.Media.TextFormatting for (var index = 0; index < textRuns.Count; index++) { - var currentRun = textRuns[index]; - - var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(runText); - var breakFound = false; - while (lineBreaker.MoveNext()) - { - if (lineBreaker.Current.Required && - currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) - { - //Explicit break found - breakFound = true; - - currentPosition = currentLength + lineBreaker.Current.PositionWrap; - - break; - } + var currentRun = textRuns[index]; - if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) - { - if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + switch (currentRun) + { + case ShapedTextRun: { - if (lastWrapPosition > 0) - { - currentPosition = lastWrapPosition; + var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - breakFound = true; + var lineBreaker = new LineBreakEnumerator(runText); - break; - } - - //Find next possible wrap position (overflow) - if (index < textRuns.Count - 1) + while (lineBreaker.MoveNext()) { - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreaker.Current.Required && + currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) { - //We already found the next possible wrap position. + //Explicit break found breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; @@ -622,51 +608,81 @@ namespace Avalonia.Media.TextFormatting break; } - while (lineBreaker.MoveNext() && index < textRuns.Count) + if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) { - currentPosition += lineBreaker.Current.PositionWrap; - - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { - break; - } + if (lastWrapPosition > 0) + { + currentPosition = lastWrapPosition; - index++; + breakFound = true; + + break; + } + + //Find next possible wrap position (overflow) + if (index < textRuns.Count - 1) + { + if (lineBreaker.Current.PositionWrap != currentRun.Length) + { + //We already found the next possible wrap position. + breakFound = true; + + currentPosition = currentLength + lineBreaker.Current.PositionWrap; + + break; + } + + while (lineBreaker.MoveNext() && index < textRuns.Count) + { + currentPosition += lineBreaker.Current.PositionWrap; + + if (lineBreaker.Current.PositionWrap != currentRun.Length) + { + break; + } + + index++; + + if (index >= textRuns.Count) + { + break; + } + + currentRun = textRuns[index]; + + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + lineBreaker = new LineBreakEnumerator(runText); + } + } + else + { + currentPosition = currentLength + lineBreaker.Current.PositionWrap; + } + + breakFound = true; - if (index >= textRuns.Count) - { break; } - currentRun = textRuns[index]; + //We overflowed so we use the last available wrap position. + currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + breakFound = true; - lineBreaker = new LineBreakEnumerator(runText); + break; } - } - else - { - currentPosition = currentLength + lineBreaker.Current.PositionWrap; - } - breakFound = true; + if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) + { + lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; + } + } break; } - - //We overflowed so we use the last available wrap position. - currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; - - breakFound = true; - - break; - } - - if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) - { - lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; - } } if (!breakFound) @@ -681,12 +697,12 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitDrawableRuns(textRuns, measuredLength); + var splitResult = SplitTextRuns(textRuns, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : + new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f803001481..ef0c726793 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + if(textLine == null || textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting break; } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + { + break; + } } //Make sure the TextLayout always contains at least on empty line diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 7b80d5ce40..e30a0fe9f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { - if (textLine.TextRuns is not List textRuns || textRuns.Count == 0) + var textRuns = textLine.TextRuns; + + if (textRuns == null || textRuns.Count == 0) { return null; } @@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return new List(0); } // Overview of ellipsis structure @@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun shapedRun: - { - currentWidth += currentRun.Size.Width; - - if (currentWidth > availableWidth) { - shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - - var collapsedRuns = new List(textRuns.Count); + currentWidth += shapedRun.Size.Width; - if (measuredLength > 0) + if (currentWidth > availableWidth) { - List? preSplitRuns = null; - List? postSplitRuns; - - if (_prefixLength > 0) - { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - collapsedRuns.AddRange(splitResult.First); + var collapsedRuns = new List(textRuns.Count); - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else + if (measuredLength > 0) { - postSplitRuns = textRuns; - } + IReadOnlyList? preSplitRuns = null; + IReadOnlyList? postSplitRuns; - collapsedRuns.Add(shapedSymbol); + if (_prefixLength > 0) + { + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, + Math.Min(_prefixLength, measuredLength)); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns; - } + collapsedRuns.AddRange(splitResult.First); - var availableSuffixWidth = availableWidth; + preSplitRuns = splitResult.First; + postSplitRuns = splitResult.Second; + } + else + { + postSplitRuns = textRuns; + } - if (preSplitRuns is not null) - { - foreach (var run in preSplitRuns) + collapsedRuns.Add(shapedSymbol); + + if (measuredLength <= _prefixLength || postSplitRuns is null) { - availableSuffixWidth -= run.Size.Width; + return collapsedRuns; } - } - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; + var availableSuffixWidth = availableWidth; - switch (run) + if (preSplitRuns is not null) { - case ShapedTextRun endShapedRun: + foreach (var run in preSplitRuns) { - if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, - out var suffixCount, out var suffixWidth)) + if (run is DrawableTextRun drawableTextRun) { - availableSuffixWidth -= suffixWidth; + availableSuffixWidth -= drawableTextRun.Size.Width; + } + } + } - if (suffixCount > 0) + for (var i = postSplitRuns.Count - 1; i >= 0; i--) + { + var run = postSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { - var splitSuffix = - endShapedRun.Split(run.Length - suffixCount); + if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, + out var suffixCount, out var suffixWidth)) + { + availableSuffixWidth -= suffixWidth; - collapsedRuns.Add(splitSuffix.Second!); - } - } + if (suffixCount > 0) + { + var splitSuffix = + endShapedRun.Split(run.Length - suffixCount); + + collapsedRuns.Add(splitSuffix.Second!); + } + } - break; + break; + } } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + else + { + collapsedRuns.Add(shapedSymbol); + } + + return collapsedRuns; } - return collapsedRuns; - } + availableWidth -= shapedRun.Size.Width; - break; - } - } + break; + } + case DrawableTextRun drawableTextRun: + { + availableWidth -= drawableTextRun.Size.Width; - availableWidth -= currentRun.Size.Width; + break; + } + } runIndex++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index ce35e47fbd..bf26ac5df4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting public class TextLineBreak { public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, - IReadOnlyList? remainingRuns = null) + IReadOnlyList? remainingRuns = null) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; @@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting /// /// Get the remaining runs that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public IReadOnlyList? RemainingRuns { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 5fb1171221..a1f93bcd07 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting { internal class TextLineImpl : TextLine { - private readonly List _textRuns; + private IReadOnlyList _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -86,11 +86,14 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - var offsetY = GetBaselineOffset(this, textRun); + if (textRun is DrawableTextRun drawable) + { + var offsetY = GetBaselineOffset(this, drawable); - textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY)); + drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY)); - currentX += textRun.Size.Width; + currentX += drawable.Size.Width; + } } } @@ -180,7 +183,14 @@ namespace Avalonia.Media.TextFormatting { var lastRun = _textRuns[_textRuns.Count - 1]; - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width); + var size = 0.0; + + if (lastRun is DrawableTextRun drawableTextRun) + { + size = drawableTextRun.Size.Width; + } + + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); } // process hit that happens within the line @@ -220,9 +230,16 @@ namespace Avalonia.Media.TextFormatting currentRun = _textRuns[j]; - if (currentDistance + currentRun.Size.Width <= distance) + if(currentRun is not ShapedTextRun) + { + continue; + } + + shapedRun = (ShapedTextRun)currentRun; + + if (currentDistance + shapedRun.Size.Width <= distance) { - currentDistance += currentRun.Size.Width; + currentDistance += shapedRun.Size.Width; currentPosition -= currentRun.Length; continue; @@ -234,12 +251,19 @@ namespace Avalonia.Media.TextFormatting characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance) + if (currentRun is DrawableTextRun drawableTextRun) { - currentDistance += currentRun.Size.Width; + if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + { + currentDistance += drawableTextRun.Size.Width; - currentPosition += currentRun.Length; + currentPosition += currentRun.Length; + continue; + } + } + else + { continue; } @@ -249,7 +273,7 @@ namespace Avalonia.Media.TextFormatting return characterHit; } - private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance) + private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance) { CharacterHit characterHit; @@ -270,9 +294,9 @@ namespace Avalonia.Media.TextFormatting break; } - default: + case DrawableTextRun drawableTextRun: { - if (distance < run.Size.Width / 2) + if (distance < drawableTextRun.Size.Width / 2) { characterHit = new CharacterHit(currentPosition); } @@ -282,6 +306,10 @@ namespace Avalonia.Media.TextFormatting } break; } + default: + characterHit = new CharacterHit(currentPosition, run.Length); + + break; } return characterHit; @@ -307,7 +335,7 @@ namespace Avalonia.Media.TextFormatting { var i = index; - var rightToLeftWidth = currentRun.Size.Width; + var rightToLeftWidth = shapedRun.Size.Width; while (i + 1 <= _textRuns.Count - 1) { @@ -317,7 +345,7 @@ namespace Avalonia.Media.TextFormatting { i++; - rightToLeftWidth += nextRun.Size.Width; + rightToLeftWidth += nextShapedRun.Size.Width; continue; } @@ -331,7 +359,10 @@ namespace Avalonia.Media.TextFormatting { currentRun = _textRuns[i]; - rightToLeftWidth -= currentRun.Size.Width; + if (currentRun is DrawableTextRun drawable) + { + rightToLeftWidth -= drawable.Size.Width; + } if (currentPosition + currentRun.Length >= characterIndex) { @@ -355,8 +386,13 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, currentDistance + distance); } + if (currentRun is DrawableTextRun drawableTextRun) + { + currentDistance += drawableTextRun.Size.Width; + } + //No hit hit found so we add the full width - currentDistance += currentRun.Size.Width; + currentPosition += currentRun.Length; remainingLength -= currentRun.Length; } @@ -380,8 +416,12 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, currentDistance - distance); } + if (currentRun is DrawableTextRun drawableTextRun) + { + currentDistance -= drawableTextRun.Size.Width; + } + //No hit hit found so we add the full width - currentDistance -= currentRun.Size.Width; currentPosition += currentRun.Length; remainingLength -= currentRun.Length; } @@ -391,7 +431,7 @@ namespace Avalonia.Media.TextFormatting } private static bool TryGetDistanceFromCharacterHit( - DrawableTextRun currentRun, + TextRun currentRun, CharacterHit characterHit, int currentPosition, int remainingLength, @@ -432,7 +472,7 @@ namespace Avalonia.Media.TextFormatting break; } - default: + case DrawableTextRun drawableTextRun: { if (characterIndex == currentPosition) { @@ -441,7 +481,7 @@ namespace Avalonia.Media.TextFormatting if (characterIndex == currentPosition + currentRun.Length) { - distance = currentRun.Size.Width; + distance = drawableTextRun.Size.Width; return true; @@ -449,6 +489,10 @@ namespace Avalonia.Media.TextFormatting break; } + default: + { + return false; + } } return false; @@ -943,7 +987,7 @@ namespace Avalonia.Media.TextFormatting return this; } - private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection) + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) { if (run is ShapedTextRun shapedTextCharacters) { @@ -1039,16 +1083,18 @@ namespace Avalonia.Media.TextFormatting minLevelToReverse--; } - _textRuns.Clear(); + var textRuns = new List(_textRuns.Count); current = orderedRun; while (current != null) { - _textRuns.Add(current.Run); + textRuns.Add(current.Run); current = current.Next; } + + _textRuns = textRuns; } /// @@ -1286,7 +1332,7 @@ namespace Avalonia.Media.TextFormatting { var runIndex = 0; textPosition = FirstTextSourceIndex; - DrawableTextRun? previousRun = null; + TextRun? previousRun = null; while (runIndex < _textRuns.Count) { @@ -1346,7 +1392,6 @@ namespace Avalonia.Media.TextFormatting break; } - default: { if (codepointIndex == textPosition) @@ -1363,6 +1408,7 @@ namespace Avalonia.Media.TextFormatting break; } + } runIndex++; @@ -1436,7 +1482,7 @@ namespace Avalonia.Media.TextFormatting break; } - case { } drawableTextRun: + case DrawableTextRun drawableTextRun: { widthIncludingWhitespace += drawableTextRun.Size.Width; @@ -1558,7 +1604,7 @@ namespace Avalonia.Media.TextFormatting private sealed class OrderedBidiRun { - public OrderedBidiRun(DrawableTextRun run, sbyte level) + public OrderedBidiRun(TextRun run, sbyte level) { Run = run; Level = level; @@ -1566,7 +1612,7 @@ namespace Avalonia.Media.TextFormatting public sbyte Level { get; } - public DrawableTextRun Run { get; } + public TextRun Run { get; } public OrderedBidiRun? Next { get; set; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index e79c2ed8b3..56232ec9c8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -40,11 +40,11 @@ namespace Avalonia.Media.TextFormatting { unsafe { - var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer; + var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length); fixed (char* charsPtr = characterBuffer.Span) { - return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length); + return new string(charsPtr, 0, _textRun.Length); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 1de04ad061..deecbbe476 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index 7c94715aa4..c291e1dfb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 5d7b8998e6..f64cf79127 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents { var text = Text ?? ""; + if (string.IsNullOrEmpty(text)) + { + return; + } + var textRunProperties = CreateTextRunProperties(); var textCharacters = new TextCharacters(text, textRunProperties); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index aa499bb135..81d7b1854b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -1,6 +1,4 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7fc27b01f4..1b6fd537eb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + private class TextSourceWithDummyRuns : ITextSource + { + private readonly TextRunProperties _properties; + private readonly List> _textRuns; + + public TextSourceWithDummyRuns(TextRunProperties properties) + { + _properties = properties; + + _textRuns = new List> + { + new ValueSpan(0, 5, new TextCharacters("Hello", _properties)), + new ValueSpan(5, 1, new DummyRun()), + new ValueSpan(6, 1, new DummyRun()), + new ValueSpan(7, 6, new TextCharacters(" World", _properties)) + }; + } + + public TextRun GetTextRun(int textSourceIndex) + { + foreach (var run in _textRuns) + { + if (textSourceIndex < run.Start + run.Length) + { + return run.Value; + } + } + + return new TextEndOfParagraph(); + } + + private class DummyRun : TextRun + { + public DummyRun() + { + Length = DefaultTextSourceLength; + } + + public override int Length { get; } + } + } + + [Fact] + public void Should_Format_TextLine_With_Non_Text_TextRuns() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new TextSourceWithDummyRuns(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(14, textLine.Length); + } + } + [Fact] public void Should_Format_TextRuns_With_TextRunStyles() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index ac2467407b..2c6ccfa896 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)); + + Assert.Equal(currentDistance, actualDistance); } }