diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 09f22612de..69b55b7222 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -134,7 +134,7 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty TextTrimmingProperty =
- AvaloniaProperty.Register(nameof(TextTrimming));
+ AvaloniaProperty.Register(nameof(TextTrimming), defaultValue: TextTrimming.None);
///
/// Defines the property.
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index c02b9d8639..b725993b44 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -52,6 +52,12 @@ MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult..ctor()'
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsInside.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsTrailing.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.TextPosition.set(System.Int32)' does not exist in the implementation but it does exist in the contract.
+CannotMakeTypeAbstract : Type 'Avalonia.Media.TextTrimming' is abstract in the implementation but is not abstract in the contract.
+TypeCannotChangeClassification : Type 'Avalonia.Media.TextTrimming' is a 'class' in the implementation but is a 'struct' in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.CharacterEllipsis' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.None' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming Avalonia.Media.TextTrimming.WordEllipsis' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Media.TextTrimming.value__' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(System.String, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableConicGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Double)' does not exist in the implementation but it does exist in the contract.
@@ -79,6 +85,9 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextC
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextCollapsingProperties.Style.get()' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Media.TextFormatting.TextCollapsingStyle' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.IBrush, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Media.TextTrimming, Avalonia.Media.TextDecorationCollection, System.Double, System.Double, System.Double, System.Int32, System.Collections.Generic.IReadOnlyList>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
@@ -124,6 +133,12 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis.Style.get()' does not exist in the implementation but it does exist in the contract.
+CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingWordEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingWordEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextTrailingWordEllipsis.Style.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.Unicode.BiDiClass' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.Unicode.BiDiClass Avalonia.Media.TextFormatting.Unicode.Codepoint.BiDiClass.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Platform.ExportRenderingSubsystemAttribute' does not exist in the implementation but it does exist in the contract.
@@ -161,4 +176,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 162
+Total Issues: 177
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index 12c40e4d59..1cac3243e3 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -854,19 +854,9 @@ namespace Avalonia.Media
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
- TextCollapsingProperties trailingEllipsis;
+ TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
- if (_that._trimming == TextTrimming.CharacterEllipsis)
- {
- trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps);
- }
- else
- {
- Debug.Assert(_that._trimming == TextTrimming.WordEllipsis);
- trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps);
- }
-
- var collapsedLine = line.Collapse(trailingEllipsis);
+ var collapsedLine = line.Collapse(collapsingProperties);
line = collapsedLine;
}
@@ -1121,11 +1111,6 @@ namespace Avalonia.Media
{
set
{
- if ((int)value < 0 || (int)value > (int)TextTrimming.WordEllipsis)
- {
- throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TextTrimming));
- }
-
_trimming = value;
_defaultParaProps.SetTextWrapping(_trimming == TextTrimming.None ?
diff --git a/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs
new file mode 100644
index 0000000000..78f15b724a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs
@@ -0,0 +1,16 @@
+using Avalonia.Media.TextFormatting;
+
+namespace Avalonia.Media
+{
+ public readonly struct TextCollapsingCreateInfo
+ {
+ public readonly double Width;
+ public readonly TextRunProperties TextRunProperties;
+
+ public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties)
+ {
+ Width = width;
+ TextRunProperties = textRunProperties;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 88ca596d2d..fb85766003 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -132,6 +132,29 @@ namespace Avalonia.Media.TextFormatting
return length > 0;
}
+ internal bool TryMeasureCharactersBackwards(double availableWidth, out int length, out double width)
+ {
+ length = 0;
+ width = 0;
+
+ for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
+ {
+ var advance = ShapedBuffer.GlyphAdvances[i];
+
+ if (width + advance > availableWidth)
+ {
+ break;
+ }
+
+ Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
+
+ length += count;
+ width += advance;
+ }
+
+ return length > 0;
+ }
+
internal SplitResult Split(int length)
{
if (IsReversed)
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
index ffd65423a3..a46f9537d0 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
@@ -1,23 +1,26 @@
-namespace Avalonia.Media.TextFormatting
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
{
///
- /// Properties of text collapsing
+ /// Properties of text collapsing.
///
public abstract class TextCollapsingProperties
{
///
- /// Gets the width in which the collapsible range is constrained to
+ /// Gets the width in which the collapsible range is constrained to.
///
public abstract double Width { get; }
///
- /// Gets the text run that is used as collapsing symbol
+ /// Gets the text run that is used as collapsing symbol.
///
public abstract TextRun Symbol { get; }
///
- /// Gets the style of collapsing
+ /// Collapses given text line.
///
- public abstract TextCollapsingStyle Style { get; }
+ /// Text line to collapse.
+ public abstract IReadOnlyList? Collapse(TextLine textLine);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs
deleted file mode 100644
index 1523cc4d9a..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Avalonia.Media.TextFormatting
-{
- ///
- /// Text collapsing style
- ///
- public enum TextCollapsingStyle
- {
- ///
- /// Collapse trailing characters
- ///
- TrailingCharacter,
-
- ///
- /// Collapse trailing words
- ///
- TrailingWord,
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
new file mode 100644
index 0000000000..2031c2ec99
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
+
+namespace Avalonia.Media.TextFormatting
+{
+ internal class TextEllipsisHelper
+ {
+ public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+ {
+ var shapedTextRuns = textLine.TextRuns as List;
+
+ if (shapedTextRuns is null)
+ {
+ return null;
+ }
+
+ var runIndex = 0;
+ var currentWidth = 0.0;
+ var collapsedLength = 0;
+ var textRange = textLine.TextRange;
+ var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
+
+ if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
+ {
+ return new List(0);
+ }
+
+ var availableWidth = properties.Width - shapedSymbol.Size.Width;
+
+ while (runIndex < shapedTextRuns.Count)
+ {
+ var currentRun = shapedTextRuns[runIndex];
+
+ currentWidth += currentRun.Size.Width;
+
+ if (currentWidth > availableWidth)
+ {
+ if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ {
+ if (isWordEllipsis && measuredLength < textRange.End)
+ {
+ var currentBreakPosition = 0;
+
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+ {
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+
+ if (nextBreakPosition == 0)
+ {
+ break;
+ }
+
+ if (nextBreakPosition >= measuredLength)
+ {
+ break;
+ }
+
+ currentBreakPosition = nextBreakPosition;
+ }
+
+ measuredLength = currentBreakPosition;
+ }
+ }
+
+ collapsedLength += measuredLength;
+
+ var shapedTextCharacters = new List(shapedTextRuns.Count);
+
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength);
+
+ shapedTextCharacters.AddRange(splitResult.First);
+
+ TextLineImpl.SortRuns(shapedTextCharacters);
+ }
+
+ shapedTextCharacters.Add(shapedSymbol);
+
+ return shapedTextCharacters;
+ }
+
+ availableWidth -= currentRun.Size.Width;
+
+ collapsedLength += currentRun.GlyphRun.Characters.Length;
+
+ runIndex++;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 4512890f08..0ff127694b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Media.TextFormatting
IBrush? foreground,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.NoWrap,
- TextTrimming textTrimming = TextTrimming.None,
+ TextTrimming? textTrimming = null,
TextDecorationCollection? textDecorations = null,
FlowDirection flowDirection = FlowDirection.LeftToRight,
double maxWidth = double.PositiveInfinity,
@@ -58,7 +58,7 @@ namespace Avalonia.Media.TextFormatting
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
- _textTrimming = textTrimming;
+ _textTrimming = textTrimming ?? TextTrimming.None;
_textStyleOverrides = textStyleOverrides;
@@ -641,14 +641,7 @@ namespace Avalonia.Media.TextFormatting
/// The .
private TextCollapsingProperties GetCollapsingProperties(double width)
{
- return _textTrimming switch
- {
- TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width,
- _paragraphProperties.DefaultTextRunProperties),
- TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width,
- _paragraphProperties.DefaultTextRunProperties),
- _ => throw new ArgumentOutOfRangeException(),
- };
+ return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
new file mode 100644
index 0000000000..74c4573630
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Ellipsis based on a fixed length leading prefix and suffix growing from the end at character granularity.
+ ///
+ public sealed class TextLeadingPrefixCharacterEllipsis : TextCollapsingProperties
+ {
+ private readonly int _prefixLength;
+
+ ///
+ /// Construct a text trailing word ellipsis collapsing properties.
+ ///
+ /// Text used as collapsing symbol.
+ /// Length of leading prefix.
+ /// width in which collapsing is constrained to
+ /// text run properties of ellispis symbol
+ public TextLeadingPrefixCharacterEllipsis(
+ ReadOnlySlice ellipsis,
+ int prefixLength,
+ double width,
+ TextRunProperties textRunProperties)
+ {
+ if (_prefixLength < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(prefixLength));
+ }
+
+ _prefixLength = prefixLength;
+ Width = width;
+ Symbol = new TextCharacters(ellipsis, textRunProperties);
+ }
+
+ ///
+ public sealed override double Width { get; }
+
+ ///
+ public sealed override TextRun Symbol { get; }
+
+ public override IReadOnlyList? Collapse(TextLine textLine)
+ {
+ var shapedTextRuns = textLine.TextRuns as List;
+
+ if (shapedTextRuns is null)
+ {
+ return null;
+ }
+
+ var runIndex = 0;
+ var currentWidth = 0.0;
+ var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight);
+
+ if (Width < shapedSymbol.GlyphRun.Size.Width)
+ {
+ return new List(0);
+ }
+
+ // Overview of ellipsis structure
+ // Prefix length run | Ellipsis symbol | Post split run growing from the end |
+ var availableWidth = Width - shapedSymbol.Size.Width;
+
+ while (runIndex < shapedTextRuns.Count)
+ {
+ var currentRun = shapedTextRuns[runIndex];
+
+ currentWidth += currentRun.Size.Width;
+
+ if (currentWidth > availableWidth)
+ {
+ currentRun.TryMeasureCharacters(availableWidth, out var measuredLength);
+
+ var shapedTextCharacters = new List(shapedTextRuns.Count);
+
+ if (measuredLength > 0)
+ {
+ List? preSplitRuns = null;
+ List? postSplitRuns = null;
+
+ if (_prefixLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength));
+
+ shapedTextCharacters.AddRange(splitResult.First);
+
+ TextLineImpl.SortRuns(shapedTextCharacters);
+
+ preSplitRuns = splitResult.First;
+ postSplitRuns = splitResult.Second;
+ }
+ else
+ {
+ postSplitRuns = shapedTextRuns;
+ }
+
+ shapedTextCharacters.Add(shapedSymbol);
+
+ if (measuredLength > _prefixLength && postSplitRuns is not null)
+ {
+ var availableSuffixWidth = availableWidth;
+
+ if (preSplitRuns is not null)
+ {
+ foreach (var run in preSplitRuns)
+ {
+ availableSuffixWidth -= run.Size.Width;
+ }
+ }
+
+ for (int i = postSplitRuns.Count - 1; i >= 0; i--)
+ {
+ var run = postSplitRuns[i];
+
+ if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth))
+ {
+ availableSuffixWidth -= suffixWidth;
+
+ if (suffixCount > 0)
+ {
+ var splitSuffix = run.Split(run.TextSourceLength - suffixCount);
+
+ shapedTextCharacters.Add(splitSuffix.Second!);
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ shapedTextCharacters.Add(shapedSymbol);
+ }
+
+ return shapedTextCharacters;
+ }
+
+ availableWidth -= currentRun.Size.Width;
+
+ runIndex++;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index e776655284..49bee6e776 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -106,85 +106,18 @@ namespace Avalonia.Media.TextFormatting
var collapsingProperties = collapsingPropertiesList[0];
- var runIndex = 0;
- var currentWidth = 0.0;
- var textRange = TextRange;
- var collapsedLength = 0;
+ var collapsedRuns = collapsingProperties.Collapse(this);
- var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection);
-
- if (collapsingProperties.Width < shapedSymbol.GlyphRun.Size.Width)
+ if (collapsedRuns is List shapedRuns)
{
- return new TextLineImpl(new List(0), textRange, _paragraphWidth, _paragraphProperties,
- _flowDirection, TextLineBreak, true);
- }
-
- var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width;
-
- while (runIndex < _textRuns.Count)
- {
- var currentRun = _textRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true);
- if (currentWidth > availableWidth)
+ if (shapedRuns.Count > 0)
{
- if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
- {
- if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord &&
- measuredLength < textRange.End)
- {
- var currentBreakPosition = 0;
-
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
-
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionMeasure;
-
- if (nextBreakPosition == 0)
- {
- break;
- }
-
- if (nextBreakPosition >= measuredLength)
- {
- break;
- }
-
- currentBreakPosition = nextBreakPosition;
- }
-
- measuredLength = currentBreakPosition;
- }
- }
-
- collapsedLength += measuredLength;
-
- var shapedTextCharacters = new List(_textRuns.Count);
-
- if (collapsedLength > 0)
- {
- var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
-
- shapedTextCharacters.AddRange(splitResult.First);
-
- SortRuns(shapedTextCharacters);
- }
-
- shapedTextCharacters.Add(shapedSymbol);
-
- var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
- _flowDirection, TextLineBreak, true);
-
- return textLine.FinalizeLine();
+ collapsedLine.FinalizeLine();
}
- availableWidth -= currentRun.Size.Width;
-
- collapsedLength += currentRun.GlyphRun.Characters.Length;
-
- runIndex++;
+ return collapsedLine;
}
return this;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index 4bd46e8c75..83acaa021e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -1,24 +1,24 @@
-using Avalonia.Utilities;
+using System.Collections.Generic;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
///
- /// a collapsing properties to collapse whole line toward the end
- /// at character granularity and with ellipsis being the collapsing symbol
+ /// A collapsing properties to collapse whole line toward the end
+ /// at character granularity.
///
- public class TextTrailingCharacterEllipsis : TextCollapsingProperties
+ public sealed class TextTrailingCharacterEllipsis : TextCollapsingProperties
{
- private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' });
-
///
/// Construct a text trailing character ellipsis collapsing properties
///
- /// width in which collapsing is constrained to
- /// text run properties of ellispis symbol
- public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties)
+ /// Text used as collapsing symbol.
+ /// Width in which collapsing is constrained to.
+ /// Text run properties of ellispis symbol.
+ public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
- Symbol = new TextCharacters(s_ellipsis, textRunProperties);
+ Symbol = new TextCharacters(ellipsis, textRunProperties);
}
///
@@ -27,7 +27,9 @@ namespace Avalonia.Media.TextFormatting
///
public sealed override TextRun Symbol { get; }
- ///
- public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter;
+ public override IReadOnlyList? Collapse(TextLine textLine)
+ {
+ return TextEllipsisHelper.Collapse(textLine, this, false);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
index 9dffddd207..ff2e4cf325 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -1,37 +1,39 @@
-using Avalonia.Utilities;
+using System.Collections.Generic;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
///
/// a collapsing properties to collapse whole line toward the end
- /// at word granularity and with ellipsis being the collapsing symbol
+ /// at word granularity.
///
- public class TextTrailingWordEllipsis : TextCollapsingProperties
+ public sealed class TextTrailingWordEllipsis : TextCollapsingProperties
{
- private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' });
-
///
- /// Construct a text trailing word ellipsis collapsing properties
+ /// Construct a text trailing word ellipsis collapsing properties.
///
- /// width in which collapsing is constrained to
- /// text run properties of ellispis symbol
+ /// Text used as collapsing symbol.
+ /// width in which collapsing is constrained to.
+ /// text run properties of ellispis symbol.
public TextTrailingWordEllipsis(
+ ReadOnlySlice ellipsis,
double width,
TextRunProperties textRunProperties
)
{
Width = width;
- Symbol = new TextCharacters(s_ellipsis, textRunProperties);
+ Symbol = new TextCharacters(ellipsis, textRunProperties);
}
-
///
public sealed override double Width { get; }
///
public sealed override TextRun Symbol { get; }
- ///
- public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord;
+ public override IReadOnlyList? Collapse(TextLine textLine)
+ {
+ return TextEllipsisHelper.Collapse(textLine, this, true);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs
new file mode 100644
index 0000000000..19ca1a0198
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs
@@ -0,0 +1,31 @@
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+ public sealed class TextLeadingPrefixTrimming : TextTrimming
+ {
+ private readonly ReadOnlySlice _ellipsis;
+ private readonly int _prefixLength;
+
+ public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
+ {
+ }
+
+ public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
+ {
+ _prefixLength = prefixLength;
+ _ellipsis = new ReadOnlySlice(ellipsis);
+ }
+
+ public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
+ {
+ return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties);
+ }
+
+ public override string ToString()
+ {
+ return nameof(PrefixCharacterEllipsis);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextNoneTrimming.cs b/src/Avalonia.Visuals/Media/TextNoneTrimming.cs
new file mode 100644
index 0000000000..ec238cf17e
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextNoneTrimming.cs
@@ -0,0 +1,18 @@
+using System;
+using Avalonia.Media.TextFormatting;
+
+namespace Avalonia.Media
+{
+ internal sealed class TextNoneTrimming : TextTrimming
+ {
+ public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override string ToString()
+ {
+ return nameof(None);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs b/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs
new file mode 100644
index 0000000000..5bb35f0ba7
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs
@@ -0,0 +1,36 @@
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+ public sealed class TextTrailingTrimming : TextTrimming
+ {
+ private readonly ReadOnlySlice _ellipsis;
+ private readonly bool _isWordBased;
+
+ public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
+ {
+ }
+
+ public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
+ {
+ _isWordBased = isWordBased;
+ _ellipsis = new ReadOnlySlice(ellipsis);
+ }
+
+ public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
+ {
+ if (_isWordBased)
+ {
+ return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
+ }
+
+ return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
+ }
+
+ public override string ToString()
+ {
+ return _isWordBased ? nameof(WordEllipsis) : nameof(CharacterEllipsis);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextTrimming.cs b/src/Avalonia.Visuals/Media/TextTrimming.cs
index f8a63dfede..b6f5be496f 100644
--- a/src/Avalonia.Visuals/Media/TextTrimming.cs
+++ b/src/Avalonia.Visuals/Media/TextTrimming.cs
@@ -1,23 +1,76 @@
-namespace Avalonia.Media
+using System;
+using Avalonia.Media.TextFormatting;
+
+namespace Avalonia.Media
{
///
/// Describes how text is trimmed when it overflows.
///
- public enum TextTrimming
+ public abstract class TextTrimming
{
+ public static char s_defaultEllipsisChar = '\u2026';
+
///
/// Text is not trimmed.
///
- None,
+ public static TextTrimming None { get; } = new TextNoneTrimming();
///
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
///
- CharacterEllipsis,
+ public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false);
///
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
///
- WordEllipsis
+ public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true);
+
+ ///
+ /// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text.
+ ///
+ public static TextTrimming PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8);
+
+ ///
+ /// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text.
+ ///
+ public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0);
+
+ ///
+ /// Creates properties that will be used for collapsing lines of text.
+ ///
+ /// Contextual info about text that will be collapsed.
+ public abstract TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo);
+
+ ///
+ /// Parses a text trimming string. Names must match static properties defined in this class.
+ ///
+ /// The text trimming string.
+ /// The .
+ public static TextTrimming Parse(string s)
+ {
+ bool Matches(string name)
+ {
+ return name.Equals(s, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (Matches(nameof(None)))
+ {
+ return None;
+ }
+ if (Matches(nameof(CharacterEllipsis)))
+ {
+ return CharacterEllipsis;
+ }
+ else if (Matches(nameof(WordEllipsis)))
+ {
+ return WordEllipsis;
+ }
+ else if (Matches(nameof(PrefixCharacterEllipsis)))
+ {
+ return PrefixCharacterEllipsis;
+ }
+
+ throw new FormatException($"Invalid text trimming string: '{s}'.");
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
index b622971d38..88529ae3a0 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
@@ -221,6 +221,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
+ if (type.Equals(types.TextTrimming))
+ {
+ foreach (var property in types.TextTrimming.Properties)
+ {
+ if (property.PropertyType == types.TextTrimming && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase))
+ {
+ result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty());
+
+ return true;
+ }
+ }
+ }
+
+ if (type.Equals(types.TextDecorationCollection))
+ {
+ foreach (var property in types.TextDecorations.Properties)
+ {
+ if (property.PropertyType == types.TextDecorationCollection && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase))
+ {
+ result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty());
+
+ return true;
+ }
+ }
+ }
+
result = null;
return false;
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 90c3989238..34a146cf37 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -88,6 +88,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ImmutableSolidColorBrush { get; }
public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; }
public IXamlType TypeUtilities { get; }
+ public IXamlType TextDecorationCollection { get; }
+ public IXamlType TextDecorations { get; }
+ public IXamlType TextTrimming { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@@ -193,6 +196,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush");
ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List { UInt });
TypeUtilities = cfg.TypeSystem.GetType("Avalonia.Utilities.TypeUtilities");
+ TextDecorationCollection = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorationCollection");
+ TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
+ TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 326997328b..7c7fb4783e 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -387,7 +387,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
- textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
+ textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] {TextTrimming.s_defaultEllipsisChar}), 300, defaultProperties));
}
currentHeight += textLine.Height;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index d1a8f175e7..367e6f4bea 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -360,12 +360,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [InlineData("01234 01234", 58, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
- [InlineData("01234 01234", 58, TextCollapsingStyle.TrailingWord, "01234\u2026")]
- [InlineData("01234", 9, TextCollapsingStyle.TrailingCharacter, "\u2026")]
- [InlineData("01234", 2, TextCollapsingStyle.TrailingCharacter, "")]
+ public static IEnumerable