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 CollapsingData + { + get + { + yield return CreateData("01234 01234 01234", 120, TextTrimming.PrefixCharacterEllipsis, "01234 01\u20264 01234"); + yield return CreateData("01234 01234", 58, TextTrimming.CharacterEllipsis, "01234 0\u2026"); + yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); + yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); + yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); + + object[] CreateData(string text, double width, TextTrimming mode, string expected) + { + return new object[] + { + text, width, mode, expected + }; + } + } + } + + [MemberData(nameof(CollapsingData))] [Theory] - public void Should_Collapse_Line(string text, double width, TextCollapsingStyle style, string expected) + public void Should_Collapse_Line(string text, double width, TextTrimming trimming, string expected) { using (Start()) { @@ -381,16 +398,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.False(textLine.HasCollapsed); - TextCollapsingProperties collapsingProperties; - - if (style == TextCollapsingStyle.TrailingCharacter) - { - collapsingProperties = new TextTrailingCharacterEllipsis(width, defaultProperties); - } - else - { - collapsingProperties = new TextTrailingWordEllipsis(width, defaultProperties); - } + TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties)); var collapsedLine = textLine.Collapse(collapsingProperties);