20 changed files with 493 additions and 158 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,18 +0,0 @@ |
|||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Text collapsing style
|
|
||||
/// </summary>
|
|
||||
public enum TextCollapsingStyle |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Collapse trailing characters
|
|
||||
/// </summary>
|
|
||||
TrailingCharacter, |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Collapse trailing words
|
|
||||
/// </summary>
|
|
||||
TrailingWord, |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,99 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal class TextEllipsisHelper |
||||
|
{ |
||||
|
public static List<ShapedTextCharacters>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) |
||||
|
{ |
||||
|
var shapedTextRuns = textLine.TextRuns as List<ShapedTextCharacters>; |
||||
|
|
||||
|
if (shapedTextRuns is null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var runIndex = 0; |
||||
|
var currentWidth = 0.0; |
||||
|
var collapsedLength = 0; |
||||
|
var textRange = textLine.TextRange; |
||||
|
|
||||
|
// TODO: From where this is supposed to come now?
|
||||
|
var flowDirection = FlowDirection.LeftToRight; |
||||
|
|
||||
|
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, flowDirection); |
||||
|
|
||||
|
if (properties.Width < shapedSymbol.GlyphRun.Size.Width) |
||||
|
{ |
||||
|
return new List<ShapedTextCharacters>(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<ShapedTextCharacters>(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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Ellipsis based on a fixed length leading prefix and suffix growing from the end.
|
||||
|
/// </summary>
|
||||
|
public class TextLeadingPrefixEllipsis : TextCollapsingProperties |
||||
|
{ |
||||
|
private readonly int _prefixLength; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Construct a text trailing word ellipsis collapsing properties
|
||||
|
/// </summary>
|
||||
|
/// <param name="width">width in which collapsing is constrained to</param>
|
||||
|
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
|
||||
|
public TextLeadingPrefixEllipsis( |
||||
|
ReadOnlySlice<char> ellipsis, |
||||
|
int prefixLength, |
||||
|
double width, |
||||
|
TextRunProperties textRunProperties |
||||
|
) |
||||
|
{ |
||||
|
_prefixLength = prefixLength; |
||||
|
Width = width; |
||||
|
Symbol = new TextCharacters(ellipsis, textRunProperties); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public sealed override double Width { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public sealed override TextRun Symbol { get; } |
||||
|
|
||||
|
public override IReadOnlyList<TextRun>? Collapse(TextLine textLine) |
||||
|
{ |
||||
|
var shapedTextRuns = textLine.TextRuns as List<ShapedTextCharacters>; |
||||
|
|
||||
|
if (shapedTextRuns is null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var runIndex = 0; |
||||
|
var currentWidth = 0.0; |
||||
|
|
||||
|
// TODO: From where this is supposed to come now?
|
||||
|
var flowDirection = FlowDirection.LeftToRight; |
||||
|
|
||||
|
var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, flowDirection); |
||||
|
|
||||
|
if (Width < shapedSymbol.GlyphRun.Size.Width) |
||||
|
{ |
||||
|
return new List<ShapedTextCharacters>(0); |
||||
|
} |
||||
|
|
||||
|
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<ShapedTextCharacters>(shapedTextRuns.Count); |
||||
|
|
||||
|
if (measuredLength > 0) |
||||
|
{ |
||||
|
var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength)); |
||||
|
|
||||
|
shapedTextCharacters.AddRange(splitResult.First); |
||||
|
|
||||
|
TextLineImpl.SortRuns(shapedTextCharacters); |
||||
|
|
||||
|
shapedTextCharacters.Add(shapedSymbol); |
||||
|
|
||||
|
if (measuredLength > _prefixLength && splitResult.Second is not null) |
||||
|
{ |
||||
|
var availableSuffixWidth = availableWidth; |
||||
|
|
||||
|
foreach (var run in splitResult.First) |
||||
|
{ |
||||
|
availableSuffixWidth -= run.Size.Width; |
||||
|
} |
||||
|
|
||||
|
for (int i = splitResult.Second.Count - 1; i >= 0; i--) |
||||
|
{ |
||||
|
var run = splitResult.Second[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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public class TextLeadingPrefixTrimming : TextTrimming |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _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<char>(ellipsis); |
||||
|
} |
||||
|
|
||||
|
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) |
||||
|
{ |
||||
|
return new TextLeadingPrefixEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return nameof(PrefixEllipsis); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
internal class TextNoneTrimming : TextTrimming |
||||
|
{ |
||||
|
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return nameof(None); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public class TextTrailingTrimming : TextTrimming |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _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<char>(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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,71 @@ |
|||||
namespace Avalonia.Media |
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Describes how text is trimmed when it overflows.
|
/// Describes how text is trimmed when it overflows.
|
||||
/// </summary>
|
/// </summary>
|
||||
public enum TextTrimming |
public abstract class TextTrimming |
||||
{ |
{ |
||||
|
public static char s_defaultEllipsisChar = '\u2026'; |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is not trimmed.
|
/// Text is not trimmed.
|
||||
/// </summary>
|
/// </summary>
|
||||
None, |
public static TextTrimming None { get; } = new TextNoneTrimming(); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
|
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
/// </summary>
|
/// </summary>
|
||||
CharacterEllipsis, |
public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
|
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
/// </summary>
|
/// </summary>
|
||||
WordEllipsis |
public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text.
|
||||
|
/// </summary>
|
||||
|
public static TextTrimming PrefixEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates properties that will be used for collapsing lines of text.
|
||||
|
/// </summary>
|
||||
|
/// <param name="createInfo">Contextual info about text that will be collapsed.</param>
|
||||
|
public abstract TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a text trimming string. Names must match static properties defined in this class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="s">The text trimming string.</param>
|
||||
|
/// <returns>The <see cref="TextTrimming"/>.</returns>
|
||||
|
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(PrefixEllipsis))) |
||||
|
{ |
||||
|
return PrefixEllipsis; |
||||
|
} |
||||
|
|
||||
|
throw new FormatException($"Invalid text trimming string: '{s}'."); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue