committed by
GitHub
60 changed files with 2569 additions and 1960 deletions
@ -0,0 +1,69 @@ |
|||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
public class GenericTextParagraphProperties : TextParagraphProperties |
||||
|
{ |
||||
|
private TextAlignment _textAlignment; |
||||
|
private TextWrapping _textWrapping; |
||||
|
private TextTrimming _textTrimming; |
||||
|
private double _lineHeight; |
||||
|
|
||||
|
public GenericTextParagraphProperties( |
||||
|
TextRunProperties defaultTextRunProperties, |
||||
|
TextAlignment textAlignment = TextAlignment.Left, |
||||
|
TextWrapping textWrapping = TextWrapping.WrapWithOverflow, |
||||
|
TextTrimming textTrimming = TextTrimming.None, |
||||
|
double lineHeight = 0) |
||||
|
{ |
||||
|
DefaultTextRunProperties = defaultTextRunProperties; |
||||
|
|
||||
|
_textAlignment = textAlignment; |
||||
|
|
||||
|
_textWrapping = textWrapping; |
||||
|
|
||||
|
_textTrimming = textTrimming; |
||||
|
|
||||
|
_lineHeight = lineHeight; |
||||
|
} |
||||
|
|
||||
|
public override TextRunProperties DefaultTextRunProperties { get; } |
||||
|
|
||||
|
public override TextAlignment TextAlignment => _textAlignment; |
||||
|
|
||||
|
public override TextWrapping TextWrapping => _textWrapping; |
||||
|
|
||||
|
public override TextTrimming TextTrimming => _textTrimming; |
||||
|
|
||||
|
public override double LineHeight => _lineHeight; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set text alignment
|
||||
|
/// </summary>
|
||||
|
internal void SetTextAlignment(TextAlignment textAlignment) |
||||
|
{ |
||||
|
_textAlignment = textAlignment; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set text wrap
|
||||
|
/// </summary>
|
||||
|
internal void SetTextWrapping(TextWrapping textWrapping) |
||||
|
{ |
||||
|
_textWrapping = textWrapping; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// Set text trimming
|
||||
|
/// </summary>
|
||||
|
internal void SetTextTrimming(TextTrimming textTrimming) |
||||
|
{ |
||||
|
_textTrimming = textTrimming; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set line height
|
||||
|
/// </summary>
|
||||
|
internal void SetLineHeight(double lineHeight) |
||||
|
{ |
||||
|
_lineHeight = lineHeight; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System.Globalization; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Generic implementation of TextRunProperties
|
||||
|
/// </summary>
|
||||
|
public class GenericTextRunProperties : TextRunProperties |
||||
|
{ |
||||
|
public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12, |
||||
|
TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null, |
||||
|
CultureInfo cultureInfo = null) |
||||
|
{ |
||||
|
Typeface = typeface; |
||||
|
FontRenderingEmSize = fontRenderingEmSize; |
||||
|
TextDecorations = textDecorations; |
||||
|
ForegroundBrush = foregroundBrush; |
||||
|
BackgroundBrush = backgroundBrush; |
||||
|
CultureInfo = cultureInfo; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override Typeface Typeface { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override double FontRenderingEmSize { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override TextDecorationCollection TextDecorations { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override IBrush ForegroundBrush { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override IBrush BackgroundBrush { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override CultureInfo CultureInfo { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A group of characters that can be shaped.
|
||||
|
/// </summary>
|
||||
|
public sealed class ShapeableTextCharacters : TextRun |
||||
|
{ |
||||
|
public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties) |
||||
|
{ |
||||
|
TextSourceLength = text.Length; |
||||
|
Text = text; |
||||
|
Properties = properties; |
||||
|
} |
||||
|
|
||||
|
public override int TextSourceLength { get; } |
||||
|
|
||||
|
public override ReadOnlySlice<char> Text { get; } |
||||
|
|
||||
|
public override TextRunProperties Properties { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A text run that holds shaped characters.
|
||||
|
/// </summary>
|
||||
|
public sealed class ShapedTextCharacters : DrawableTextRun |
||||
|
{ |
||||
|
public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties) |
||||
|
{ |
||||
|
Text = glyphRun.Characters; |
||||
|
Properties = properties; |
||||
|
TextSourceLength = Text.Length; |
||||
|
FontMetrics = new FontMetrics(Properties.Typeface, Properties.FontRenderingEmSize); |
||||
|
GlyphRun = glyphRun; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override ReadOnlySlice<char> Text { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override TextRunProperties Properties { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int TextSourceLength { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override Rect Bounds => GlyphRun.Bounds; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the font metrics.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The font metrics.
|
||||
|
/// </value>
|
||||
|
public FontMetrics FontMetrics { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the glyph run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The glyphs.
|
||||
|
/// </value>
|
||||
|
public GlyphRun GlyphRun { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Draw(DrawingContext drawingContext, Point origin) |
||||
|
{ |
||||
|
if (GlyphRun.GlyphIndices.Length == 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Properties.Typeface == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Properties.ForegroundBrush == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (Properties.BackgroundBrush != null) |
||||
|
{ |
||||
|
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, |
||||
|
new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); |
||||
|
} |
||||
|
|
||||
|
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); |
||||
|
|
||||
|
if (Properties.TextDecorations == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
foreach (var textDecoration in Properties.TextDecorations) |
||||
|
{ |
||||
|
textDecoration.Draw(drawingContext, this, origin); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Splits the <see cref="TextRun"/> at specified length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The length.</param>
|
||||
|
/// <returns>The split result.</returns>
|
||||
|
public SplitTextCharactersResult Split(int length) |
||||
|
{ |
||||
|
var glyphCount = 0; |
||||
|
|
||||
|
var firstCharacters = GlyphRun.Characters.Take(length); |
||||
|
|
||||
|
var codepointEnumerator = new CodepointEnumerator(firstCharacters); |
||||
|
|
||||
|
while (codepointEnumerator.MoveNext()) |
||||
|
{ |
||||
|
glyphCount++; |
||||
|
} |
||||
|
|
||||
|
if (GlyphRun.Characters.Length == length) |
||||
|
{ |
||||
|
return new SplitTextCharactersResult(this, null); |
||||
|
} |
||||
|
|
||||
|
if (GlyphRun.GlyphIndices.Length == glyphCount) |
||||
|
{ |
||||
|
return new SplitTextCharactersResult(this, null); |
||||
|
} |
||||
|
|
||||
|
var firstGlyphRun = new GlyphRun( |
||||
|
Properties.Typeface.GlyphTypeface, |
||||
|
Properties.FontRenderingEmSize, |
||||
|
GlyphRun.GlyphIndices.Take(glyphCount), |
||||
|
GlyphRun.GlyphAdvances.Take(glyphCount), |
||||
|
GlyphRun.GlyphOffsets.Take(glyphCount), |
||||
|
GlyphRun.Characters.Take(length), |
||||
|
GlyphRun.GlyphClusters.Take(glyphCount)); |
||||
|
|
||||
|
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); |
||||
|
|
||||
|
var secondGlyphRun = new GlyphRun( |
||||
|
Properties.Typeface.GlyphTypeface, |
||||
|
Properties.FontRenderingEmSize, |
||||
|
GlyphRun.GlyphIndices.Skip(glyphCount), |
||||
|
GlyphRun.GlyphAdvances.Skip(glyphCount), |
||||
|
GlyphRun.GlyphOffsets.Skip(glyphCount), |
||||
|
GlyphRun.Characters.Skip(length), |
||||
|
GlyphRun.GlyphClusters.Skip(glyphCount)); |
||||
|
|
||||
|
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); |
||||
|
|
||||
|
return new SplitTextCharactersResult(firstTextRun, secondTextRun); |
||||
|
} |
||||
|
|
||||
|
public readonly struct SplitTextCharactersResult |
||||
|
{ |
||||
|
public SplitTextCharactersResult(ShapedTextCharacters first, ShapedTextCharacters second) |
||||
|
{ |
||||
|
First = first; |
||||
|
|
||||
|
Second = second; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the first text run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The first text run.
|
||||
|
/// </value>
|
||||
|
public ShapedTextCharacters First { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the second text run.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The second text run.
|
||||
|
/// </value>
|
||||
|
public ShapedTextCharacters Second { get; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,212 +0,0 @@ |
|||||
using Avalonia.Media.Immutable; |
|
||||
using Avalonia.Media.TextFormatting.Unicode; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utility; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A text run that holds a shaped glyph run.
|
|
||||
/// </summary>
|
|
||||
public sealed class ShapedTextRun : DrawableTextRun |
|
||||
{ |
|
||||
public ShapedTextRun(ReadOnlySlice<char> text, TextStyle style) : this( |
|
||||
TextShaper.Current.ShapeText(text, style.TextFormat), style) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public ShapedTextRun(GlyphRun glyphRun, TextStyle style) |
|
||||
{ |
|
||||
Text = glyphRun.Characters; |
|
||||
Style = style; |
|
||||
GlyphRun = glyphRun; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override Rect Bounds => GlyphRun.Bounds; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the glyph run.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The glyphs.
|
|
||||
/// </value>
|
|
||||
public GlyphRun GlyphRun { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override void Draw(IDrawingContextImpl drawingContext, Point origin) |
|
||||
{ |
|
||||
if (GlyphRun.GlyphIndices.Length == 0) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (Style.TextFormat.Typeface == null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (Style.Foreground == null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
drawingContext.DrawGlyphRun(Style.Foreground, GlyphRun, origin); |
|
||||
|
|
||||
if (Style.TextDecorations == null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
foreach (var textDecoration in Style.TextDecorations) |
|
||||
{ |
|
||||
DrawTextDecoration(drawingContext, textDecoration, origin); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Draws the <see cref="TextDecoration"/> at given origin.
|
|
||||
/// </summary>
|
|
||||
/// <param name="drawingContext">The drawing context.</param>
|
|
||||
/// <param name="textDecoration">The text decoration.</param>
|
|
||||
/// <param name="origin">The origin.</param>
|
|
||||
private void DrawTextDecoration(IDrawingContextImpl drawingContext, ImmutableTextDecoration textDecoration, Point origin) |
|
||||
{ |
|
||||
var textFormat = Style.TextFormat; |
|
||||
|
|
||||
var fontMetrics = Style.TextFormat.FontMetrics; |
|
||||
|
|
||||
var thickness = textDecoration.Pen?.Thickness ?? 1.0; |
|
||||
|
|
||||
switch (textDecoration.PenThicknessUnit) |
|
||||
{ |
|
||||
case TextDecorationUnit.FontRecommended: |
|
||||
switch (textDecoration.Location) |
|
||||
{ |
|
||||
case TextDecorationLocation.Underline: |
|
||||
thickness = fontMetrics.UnderlineThickness; |
|
||||
break; |
|
||||
case TextDecorationLocation.Strikethrough: |
|
||||
thickness = fontMetrics.StrikethroughThickness; |
|
||||
break; |
|
||||
} |
|
||||
break; |
|
||||
case TextDecorationUnit.FontRenderingEmSize: |
|
||||
thickness = textFormat.FontRenderingEmSize * thickness; |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
switch (textDecoration.Location) |
|
||||
{ |
|
||||
case TextDecorationLocation.Overline: |
|
||||
origin += new Point(0, textFormat.FontMetrics.Ascent); |
|
||||
break; |
|
||||
case TextDecorationLocation.Strikethrough: |
|
||||
origin += new Point(0, -textFormat.FontMetrics.StrikethroughPosition); |
|
||||
break; |
|
||||
case TextDecorationLocation.Underline: |
|
||||
origin += new Point(0, -textFormat.FontMetrics.UnderlinePosition); |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
switch (textDecoration.PenOffsetUnit) |
|
||||
{ |
|
||||
case TextDecorationUnit.FontRenderingEmSize: |
|
||||
origin += new Point(0, textDecoration.PenOffset * textFormat.FontRenderingEmSize); |
|
||||
break; |
|
||||
case TextDecorationUnit.Pixel: |
|
||||
origin += new Point(0, textDecoration.PenOffset); |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
var pen = new ImmutablePen( |
|
||||
textDecoration.Pen?.Brush ?? Style.Foreground.ToImmutable(), |
|
||||
thickness, |
|
||||
textDecoration.Pen?.DashStyle?.ToImmutable(), |
|
||||
textDecoration.Pen?.LineCap ?? default, |
|
||||
textDecoration.Pen?.LineJoin ?? PenLineJoin.Miter, |
|
||||
textDecoration.Pen?.MiterLimit ?? 10.0); |
|
||||
|
|
||||
drawingContext.DrawLine(pen, origin, origin + new Point(GlyphRun.Bounds.Width, 0)); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Splits the <see cref="TextRun"/> at specified length.
|
|
||||
/// </summary>
|
|
||||
/// <param name="length">The length.</param>
|
|
||||
/// <returns>The split result.</returns>
|
|
||||
public SplitTextCharactersResult Split(int length) |
|
||||
{ |
|
||||
var glyphCount = 0; |
|
||||
|
|
||||
var firstCharacters = GlyphRun.Characters.Take(length); |
|
||||
|
|
||||
var codepointEnumerator = new CodepointEnumerator(firstCharacters); |
|
||||
|
|
||||
while (codepointEnumerator.MoveNext()) |
|
||||
{ |
|
||||
glyphCount++; |
|
||||
} |
|
||||
|
|
||||
if (GlyphRun.Characters.Length == length) |
|
||||
{ |
|
||||
return new SplitTextCharactersResult(this, null); |
|
||||
} |
|
||||
|
|
||||
if (GlyphRun.GlyphIndices.Length == glyphCount) |
|
||||
{ |
|
||||
return new SplitTextCharactersResult(this, null); |
|
||||
} |
|
||||
|
|
||||
var firstGlyphRun = new GlyphRun( |
|
||||
Style.TextFormat.Typeface.GlyphTypeface, |
|
||||
Style.TextFormat.FontRenderingEmSize, |
|
||||
GlyphRun.GlyphIndices.Take(glyphCount), |
|
||||
GlyphRun.GlyphAdvances.Take(glyphCount), |
|
||||
GlyphRun.GlyphOffsets.Take(glyphCount), |
|
||||
GlyphRun.Characters.Take(length), |
|
||||
GlyphRun.GlyphClusters.Take(length)); |
|
||||
|
|
||||
var firstTextRun = new ShapedTextRun(firstGlyphRun, Style); |
|
||||
|
|
||||
var secondGlyphRun = new GlyphRun( |
|
||||
Style.TextFormat.Typeface.GlyphTypeface, |
|
||||
Style.TextFormat.FontRenderingEmSize, |
|
||||
GlyphRun.GlyphIndices.Skip(glyphCount), |
|
||||
GlyphRun.GlyphAdvances.Skip(glyphCount), |
|
||||
GlyphRun.GlyphOffsets.Skip(glyphCount), |
|
||||
GlyphRun.Characters.Skip(length), |
|
||||
GlyphRun.GlyphClusters.Skip(length)); |
|
||||
|
|
||||
var secondTextRun = new ShapedTextRun(secondGlyphRun, Style); |
|
||||
|
|
||||
return new SplitTextCharactersResult(firstTextRun, secondTextRun); |
|
||||
} |
|
||||
|
|
||||
public readonly struct SplitTextCharactersResult |
|
||||
{ |
|
||||
public SplitTextCharactersResult(ShapedTextRun first, ShapedTextRun second) |
|
||||
{ |
|
||||
First = first; |
|
||||
|
|
||||
Second = second; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the first text run.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The first text run.
|
|
||||
/// </value>
|
|
||||
public ShapedTextRun First { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the second text run.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The second text run.
|
|
||||
/// </value>
|
|
||||
public ShapedTextRun Second { get; } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,395 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media.TextFormatting.Unicode; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utility; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
internal class SimpleTextFormatter : TextFormatter |
|
||||
{ |
|
||||
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); |
|
||||
|
|
||||
/// <inheritdoc cref="TextFormatter.FormatLine"/>
|
|
||||
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, |
|
||||
TextParagraphProperties paragraphProperties) |
|
||||
{ |
|
||||
var textTrimming = paragraphProperties.TextTrimming; |
|
||||
var textWrapping = paragraphProperties.TextWrapping; |
|
||||
TextLine textLine; |
|
||||
|
|
||||
var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer); |
|
||||
|
|
||||
if (textTrimming != TextTrimming.None) |
|
||||
{ |
|
||||
textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
if (textWrapping == TextWrapping.Wrap) |
|
||||
{ |
|
||||
textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var textLineMetrics = |
|
||||
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment); |
|
||||
|
|
||||
textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return textLine; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Formats text runs with optional text style overrides.
|
|
||||
/// </summary>
|
|
||||
/// <param name="textSource">The text source.</param>
|
|
||||
/// <param name="firstTextSourceIndex">The first text source index.</param>
|
|
||||
/// <param name="textPointer">The text pointer that covers the formatted text runs.</param>
|
|
||||
/// <returns>
|
|
||||
/// The formatted text runs.
|
|
||||
/// </returns>
|
|
||||
private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) |
|
||||
{ |
|
||||
var start = -1; |
|
||||
var length = 0; |
|
||||
|
|
||||
var textRuns = new List<ShapedTextRun>(); |
|
||||
|
|
||||
while (true) |
|
||||
{ |
|
||||
var textRun = textSource.GetTextRun(firstTextSourceIndex + length); |
|
||||
|
|
||||
if (start == -1) |
|
||||
{ |
|
||||
start = textRun.Text.Start; |
|
||||
} |
|
||||
|
|
||||
if (textRun is TextEndOfLine) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
switch (textRun) |
|
||||
{ |
|
||||
case TextCharacters textCharacters: |
|
||||
|
|
||||
var runText = textCharacters.Text; |
|
||||
|
|
||||
while (!runText.IsEmpty) |
|
||||
{ |
|
||||
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); |
|
||||
|
|
||||
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), |
|
||||
shapableTextStyleRun.Style); |
|
||||
|
|
||||
textRuns.Add(shapedRun); |
|
||||
|
|
||||
runText = runText.Skip(shapedRun.Text.Length); |
|
||||
} |
|
||||
|
|
||||
break; |
|
||||
default: |
|
||||
throw new NotSupportedException("Run type not supported by the formatter."); |
|
||||
} |
|
||||
|
|
||||
length += textRun.Text.Length; |
|
||||
} |
|
||||
|
|
||||
textPointer = new TextPointer(start, length); |
|
||||
|
|
||||
return textRuns; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Performs text trimming and returns a trimmed line.
|
|
||||
/// </summary>
|
|
||||
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
|
|
||||
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
|
|
||||
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
|
|
||||
/// <param name="textRuns">The text runs to perform the trimming on.</param>
|
|
||||
/// <param name="text">The text that was used to construct the text runs.</param>
|
|
||||
/// <returns></returns>
|
|
||||
private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, |
|
||||
double paragraphWidth, TextParagraphProperties paragraphProperties) |
|
||||
{ |
|
||||
var textTrimming = paragraphProperties.TextTrimming; |
|
||||
var availableWidth = paragraphWidth; |
|
||||
var currentWidth = 0.0; |
|
||||
var runIndex = 0; |
|
||||
|
|
||||
while (runIndex < textRuns.Count) |
|
||||
{ |
|
||||
var currentRun = textRuns[runIndex]; |
|
||||
|
|
||||
currentWidth += currentRun.GlyphRun.Bounds.Width; |
|
||||
|
|
||||
if (currentWidth > availableWidth) |
|
||||
{ |
|
||||
var ellipsisRun = CreateEllipsisRun(currentRun.Style); |
|
||||
|
|
||||
var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); |
|
||||
|
|
||||
if (textTrimming == TextTrimming.WordEllipsis) |
|
||||
{ |
|
||||
if (measuredLength < text.End) |
|
||||
{ |
|
||||
var currentBreakPosition = 0; |
|
||||
|
|
||||
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
|
||||
|
|
||||
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
|
||||
{ |
|
||||
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
|
||||
|
|
||||
if (nextBreakPosition == 0) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if (nextBreakPosition > measuredLength) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
currentBreakPosition = nextBreakPosition; |
|
||||
} |
|
||||
|
|
||||
measuredLength = currentBreakPosition; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
var splitResult = SplitTextRuns(textRuns, measuredLength); |
|
||||
|
|
||||
var trimmedRuns = new List<ShapedTextRun>(splitResult.First.Count + 1); |
|
||||
|
|
||||
trimmedRuns.AddRange(splitResult.First); |
|
||||
|
|
||||
trimmedRuns.Add(ellipsisRun); |
|
||||
|
|
||||
var textLineMetrics = |
|
||||
TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); |
|
||||
|
|
||||
return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics); |
|
||||
} |
|
||||
|
|
||||
availableWidth -= currentRun.GlyphRun.Bounds.Width; |
|
||||
|
|
||||
runIndex++; |
|
||||
} |
|
||||
|
|
||||
return new SimpleTextLine(text, textRuns, |
|
||||
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Performs text wrapping returns a list of text lines.
|
|
||||
/// </summary>
|
|
||||
/// <param name="paragraphProperties">The text paragraph properties.</param>
|
|
||||
/// <param name="textRuns">The text run'S.</param>
|
|
||||
/// <param name="text">The text to analyze for break opportunities.</param>
|
|
||||
/// <param name="paragraphWidth"></param>
|
|
||||
/// <returns></returns>
|
|
||||
private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, |
|
||||
double paragraphWidth, TextParagraphProperties paragraphProperties) |
|
||||
{ |
|
||||
var availableWidth = paragraphWidth; |
|
||||
var currentWidth = 0.0; |
|
||||
var runIndex = 0; |
|
||||
var length = 0; |
|
||||
|
|
||||
while (runIndex < textRuns.Count) |
|
||||
{ |
|
||||
var currentRun = textRuns[runIndex]; |
|
||||
|
|
||||
if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) |
|
||||
{ |
|
||||
var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); |
|
||||
|
|
||||
if (measuredLength < currentRun.Text.Length) |
|
||||
{ |
|
||||
var currentBreakPosition = -1; |
|
||||
|
|
||||
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
|
||||
|
|
||||
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
|
||||
{ |
|
||||
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
|
||||
|
|
||||
if (nextBreakPosition == 0) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if (nextBreakPosition > measuredLength) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
currentBreakPosition = nextBreakPosition; |
|
||||
} |
|
||||
|
|
||||
if (currentBreakPosition != -1) |
|
||||
{ |
|
||||
measuredLength = currentBreakPosition; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
length += measuredLength; |
|
||||
|
|
||||
var splitResult = SplitTextRuns(textRuns, length); |
|
||||
|
|
||||
var textLineMetrics = |
|
||||
TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); |
|
||||
|
|
||||
return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics); |
|
||||
} |
|
||||
|
|
||||
currentWidth += currentRun.GlyphRun.Bounds.Width; |
|
||||
|
|
||||
length += currentRun.GlyphRun.Characters.Length; |
|
||||
|
|
||||
runIndex++; |
|
||||
} |
|
||||
|
|
||||
return new SimpleTextLine(text, textRuns, |
|
||||
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Measures the number of characters that fits into available width.
|
|
||||
/// </summary>
|
|
||||
/// <param name="textRun">The text run.</param>
|
|
||||
/// <param name="availableWidth">The available width.</param>
|
|
||||
/// <returns></returns>
|
|
||||
private static int MeasureText(ShapedTextRun textRun, double availableWidth) |
|
||||
{ |
|
||||
var glyphRun = textRun.GlyphRun; |
|
||||
|
|
||||
var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); |
|
||||
|
|
||||
return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates an ellipsis.
|
|
||||
/// </summary>
|
|
||||
/// <param name="textStyle">The text style.</param>
|
|
||||
/// <returns></returns>
|
|
||||
private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle) |
|
||||
{ |
|
||||
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>(); |
|
||||
|
|
||||
var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat); |
|
||||
|
|
||||
return new ShapedTextRun(glyphRun, textStyle); |
|
||||
} |
|
||||
|
|
||||
private readonly struct SplitTextRunsResult |
|
||||
{ |
|
||||
public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second) |
|
||||
{ |
|
||||
First = first; |
|
||||
|
|
||||
Second = second; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the first text runs.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The first text runs.
|
|
||||
/// </value>
|
|
||||
public IReadOnlyList<ShapedTextRun> First { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the second text runs.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The second text runs.
|
|
||||
/// </value>
|
|
||||
public IReadOnlyList<ShapedTextRun> Second { get; } |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Split a sequence of runs into two segments at specified length.
|
|
||||
/// </summary>
|
|
||||
/// <param name="textRuns">The text run's.</param>
|
|
||||
/// <param name="length">The length to split at.</param>
|
|
||||
/// <returns></returns>
|
|
||||
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length) |
|
||||
{ |
|
||||
var currentLength = 0; |
|
||||
|
|
||||
for (var i = 0; i < textRuns.Count; i++) |
|
||||
{ |
|
||||
var currentRun = textRuns[i]; |
|
||||
|
|
||||
if (currentLength + currentRun.GlyphRun.Characters.Length < length) |
|
||||
{ |
|
||||
currentLength += currentRun.GlyphRun.Characters.Length; |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; |
|
||||
|
|
||||
var first = new ShapedTextRun[firstCount]; |
|
||||
|
|
||||
if (firstCount > 1) |
|
||||
{ |
|
||||
for (var j = 0; j < i; j++) |
|
||||
{ |
|
||||
first[j] = textRuns[j]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
var secondCount = textRuns.Count - firstCount; |
|
||||
|
|
||||
if (currentLength + currentRun.GlyphRun.Characters.Length == length) |
|
||||
{ |
|
||||
var second = new ShapedTextRun[secondCount]; |
|
||||
|
|
||||
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; |
|
||||
|
|
||||
if (secondCount > 0) |
|
||||
{ |
|
||||
for (var j = 0; j < secondCount; j++) |
|
||||
{ |
|
||||
second[j] = textRuns[i + j + offset]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
first[i] = currentRun; |
|
||||
|
|
||||
return new SplitTextRunsResult(first, second); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
secondCount++; |
|
||||
|
|
||||
var second = new ShapedTextRun[secondCount]; |
|
||||
|
|
||||
if (secondCount > 0) |
|
||||
{ |
|
||||
for (var j = 1; j < secondCount; j++) |
|
||||
{ |
|
||||
second[j] = textRuns[i + j]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
var split = currentRun.Split(length - currentLength); |
|
||||
|
|
||||
first[i] = split.First; |
|
||||
|
|
||||
second[0] = split.Second; |
|
||||
|
|
||||
return new SplitTextRunsResult(first, second); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return new SplitTextRunsResult(textRuns, null); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,259 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
internal class SimpleTextLine : TextLine |
|
||||
{ |
|
||||
private readonly IReadOnlyList<ShapedTextRun> _textRuns; |
|
||||
|
|
||||
public SimpleTextLine(TextPointer textPointer, IReadOnlyList<ShapedTextRun> textRuns, TextLineMetrics lineMetrics) |
|
||||
{ |
|
||||
Text = textPointer; |
|
||||
_textRuns = textRuns; |
|
||||
LineMetrics = lineMetrics; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override TextPointer Text { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override IReadOnlyList<TextRun> TextRuns => _textRuns; |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override TextLineMetrics LineMetrics { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override void Draw(IDrawingContextImpl drawingContext, Point origin) |
|
||||
{ |
|
||||
var currentX = origin.X; |
|
||||
|
|
||||
foreach (var textRun in _textRuns) |
|
||||
{ |
|
||||
var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, |
|
||||
origin.Y + LineMetrics.BaselineOrigin.Y); |
|
||||
|
|
||||
textRun.Draw(drawingContext, baselineOrigin); |
|
||||
|
|
||||
currentX += textRun.Bounds.Width; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override CharacterHit GetCharacterHitFromDistance(double distance) |
|
||||
{ |
|
||||
if (distance < 0) |
|
||||
{ |
|
||||
// hit happens before the line, return the first position
|
|
||||
return new CharacterHit(Text.Start); |
|
||||
} |
|
||||
|
|
||||
// process hit that happens within the line
|
|
||||
var characterHit = new CharacterHit(); |
|
||||
|
|
||||
foreach (var run in _textRuns) |
|
||||
{ |
|
||||
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); |
|
||||
|
|
||||
if (distance <= run.Bounds.Width) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
distance -= run.Bounds.Width; |
|
||||
} |
|
||||
|
|
||||
return characterHit; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) |
|
||||
{ |
|
||||
return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) |
|
||||
{ |
|
||||
int nextVisibleCp; |
|
||||
bool navigableCpFound; |
|
||||
|
|
||||
if (characterHit.TrailingLength == 0) |
|
||||
{ |
|
||||
navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp); |
|
||||
|
|
||||
if (navigableCpFound) |
|
||||
{ |
|
||||
// Move from leading to trailing edge
|
|
||||
return new CharacterHit(nextVisibleCp, 1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); |
|
||||
|
|
||||
if (navigableCpFound) |
|
||||
{ |
|
||||
// Move from trailing edge of current character to trailing edge of next
|
|
||||
return new CharacterHit(nextVisibleCp, 1); |
|
||||
} |
|
||||
|
|
||||
// Can't move, we're after the last character
|
|
||||
return characterHit; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) |
|
||||
{ |
|
||||
int previousCodepointIndex; |
|
||||
bool codepointIndexFound; |
|
||||
|
|
||||
var cpHit = characterHit.FirstCharacterIndex; |
|
||||
var trailingHit = characterHit.TrailingLength != 0; |
|
||||
|
|
||||
// Input can be right after the end of the current line. Snap it to be at the end of the line.
|
|
||||
if (cpHit >= Text.Start + Text.Length) |
|
||||
{ |
|
||||
cpHit = Text.Start + Text.Length - 1; |
|
||||
|
|
||||
trailingHit = true; |
|
||||
} |
|
||||
|
|
||||
if (trailingHit) |
|
||||
{ |
|
||||
codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex); |
|
||||
|
|
||||
if (codepointIndexFound) |
|
||||
{ |
|
||||
// Move from trailing to leading edge
|
|
||||
return new CharacterHit(previousCodepointIndex, 0); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex); |
|
||||
|
|
||||
if (codepointIndexFound) |
|
||||
{ |
|
||||
// Move from leading edge of current character to leading edge of previous
|
|
||||
return new CharacterHit(previousCodepointIndex, 0); |
|
||||
} |
|
||||
|
|
||||
// Can't move, we're before the first character
|
|
||||
return characterHit; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) |
|
||||
{ |
|
||||
// same operation as move-to-previous
|
|
||||
return GetPreviousCaretCharacterHit(characterHit); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Get distance from line start to the specified codepoint index
|
|
||||
/// </summary>
|
|
||||
private double DistanceFromCodepointIndex(int codepointIndex) |
|
||||
{ |
|
||||
var currentDistance = 0.0; |
|
||||
|
|
||||
foreach (var textRun in _textRuns) |
|
||||
{ |
|
||||
if (codepointIndex > textRun.Text.End) |
|
||||
{ |
|
||||
currentDistance += textRun.Bounds.Width; |
|
||||
|
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); |
|
||||
} |
|
||||
|
|
||||
return currentDistance; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index.
|
|
||||
/// Return true if one such codepoint index is found, false otherwise.
|
|
||||
/// </summary>
|
|
||||
private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex) |
|
||||
{ |
|
||||
nextCodepointIndex = codepointIndex; |
|
||||
|
|
||||
if (codepointIndex >= Text.Start + Text.Length) |
|
||||
{ |
|
||||
return false; // Cannot go forward anymore
|
|
||||
} |
|
||||
|
|
||||
GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart); |
|
||||
|
|
||||
while (runIndex < TextRuns.Count) |
|
||||
{ |
|
||||
// When navigating forward, only the trailing edge of visible content is
|
|
||||
// navigable.
|
|
||||
if (runIndex < TextRuns.Count) |
|
||||
{ |
|
||||
nextCodepointIndex = Math.Max(cpRunStart, codepointIndex); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
cpRunStart += TextRuns[runIndex++].Text.Length; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index.
|
|
||||
/// Return true if one such codepoint is found, false otherwise.
|
|
||||
/// </summary>
|
|
||||
private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex) |
|
||||
{ |
|
||||
previousCodepointIndex = codepointIndex; |
|
||||
|
|
||||
if (codepointIndex < Text.Start) |
|
||||
{ |
|
||||
return false; // Cannot go backward anymore.
|
|
||||
} |
|
||||
|
|
||||
// Position the cpRunEnd at the end of the span that contains the given cp
|
|
||||
GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd); |
|
||||
|
|
||||
codepointIndexAtRunEnd += TextRuns[runIndex].Text.End; |
|
||||
|
|
||||
while (runIndex >= 0) |
|
||||
{ |
|
||||
// Visible content has caret stops at its leading edge.
|
|
||||
if (runIndex + 1 < TextRuns.Count) |
|
||||
{ |
|
||||
previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Newline sequence has caret stops at its leading edge.
|
|
||||
if (runIndex == TextRuns.Count) |
|
||||
{ |
|
||||
// Get the cp index at the beginning of the newline sequence.
|
|
||||
previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1; |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart) |
|
||||
{ |
|
||||
codepointIndexAtRunStart = Text.Start; |
|
||||
runIndex = 0; |
|
||||
|
|
||||
// Find the span that contains the given cp
|
|
||||
while (runIndex < TextRuns.Count && |
|
||||
codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex) |
|
||||
{ |
|
||||
codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,71 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Unique text formatting properties that are used by the <see cref="TextFormatter"/>.
|
|
||||
/// </summary>
|
|
||||
public readonly struct TextFormat : IEquatable<TextFormat> |
|
||||
{ |
|
||||
public TextFormat(Typeface typeface, double fontRenderingEmSize) |
|
||||
{ |
|
||||
Typeface = typeface; |
|
||||
FontRenderingEmSize = fontRenderingEmSize; |
|
||||
FontMetrics = new FontMetrics(typeface, fontRenderingEmSize); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the typeface.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The typeface.
|
|
||||
/// </value>
|
|
||||
public Typeface Typeface { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the font rendering em size.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The em rendering size of the font.
|
|
||||
/// </value>
|
|
||||
public double FontRenderingEmSize { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the font metrics.
|
|
||||
/// </summary>
|
|
||||
/// <value>
|
|
||||
/// The metrics of the font.
|
|
||||
/// </value>
|
|
||||
public FontMetrics FontMetrics { get; } |
|
||||
|
|
||||
public static bool operator ==(TextFormat self, TextFormat other) |
|
||||
{ |
|
||||
return self.Equals(other); |
|
||||
} |
|
||||
|
|
||||
public static bool operator !=(TextFormat self, TextFormat other) |
|
||||
{ |
|
||||
return !(self == other); |
|
||||
} |
|
||||
|
|
||||
public bool Equals(TextFormat other) |
|
||||
{ |
|
||||
return Typeface.Equals(other.Typeface) && FontRenderingEmSize.Equals(other.FontRenderingEmSize); |
|
||||
} |
|
||||
|
|
||||
public override bool Equals(object obj) |
|
||||
{ |
|
||||
return obj is TextFormat other && Equals(other); |
|
||||
} |
|
||||
|
|
||||
public override int GetHashCode() |
|
||||
{ |
|
||||
unchecked |
|
||||
{ |
|
||||
var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); |
|
||||
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); |
|
||||
return hashCode; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,544 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal class TextFormatterImpl : TextFormatter |
||||
|
{ |
||||
|
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); |
||||
|
|
||||
|
/// <inheritdoc cref="TextFormatter.FormatLine"/>
|
||||
|
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, |
||||
|
TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) |
||||
|
{ |
||||
|
var textTrimming = paragraphProperties.TextTrimming; |
||||
|
var textWrapping = paragraphProperties.TextWrapping; |
||||
|
TextLine textLine = null; |
||||
|
|
||||
|
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); |
||||
|
|
||||
|
var textRange = GetTextRange(textRuns); |
||||
|
|
||||
|
if (textTrimming != TextTrimming.None) |
||||
|
{ |
||||
|
textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
switch (textWrapping) |
||||
|
{ |
||||
|
case TextWrapping.NoWrap: |
||||
|
{ |
||||
|
var textLineMetrics = |
||||
|
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties); |
||||
|
|
||||
|
textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak); |
||||
|
break; |
||||
|
} |
||||
|
case TextWrapping.WrapWithOverflow: |
||||
|
case TextWrapping.Wrap: |
||||
|
{ |
||||
|
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return textLine; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fetches text runs.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textSource">The text source.</param>
|
||||
|
/// <param name="firstTextSourceIndex">The first text source index.</param>
|
||||
|
/// <param name="previousLineBreak">Previous line break. Can be null.</param>
|
||||
|
/// <param name="nextLineBreak">Next line break. Can be null.</param>
|
||||
|
/// <returns>
|
||||
|
/// The formatted text runs.
|
||||
|
/// </returns>
|
||||
|
private static IReadOnlyList<ShapedTextCharacters> FetchTextRuns(ITextSource textSource, |
||||
|
int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) |
||||
|
{ |
||||
|
nextLineBreak = default; |
||||
|
|
||||
|
var currentLength = 0; |
||||
|
|
||||
|
var textRuns = new List<ShapedTextCharacters>(); |
||||
|
|
||||
|
if (previousLineBreak != null) |
||||
|
{ |
||||
|
foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) |
||||
|
{ |
||||
|
textRuns.Add(shapedCharacters); |
||||
|
|
||||
|
if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) |
||||
|
{ |
||||
|
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); |
||||
|
|
||||
|
nextLineBreak = new TextLineBreak(splitResult.Second); |
||||
|
|
||||
|
return splitResult.First; |
||||
|
} |
||||
|
|
||||
|
currentLength += shapedCharacters.Text.Length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
firstTextSourceIndex += currentLength; |
||||
|
|
||||
|
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); |
||||
|
|
||||
|
while (textRunEnumerator.MoveNext()) |
||||
|
{ |
||||
|
var textRun = textRunEnumerator.Current; |
||||
|
|
||||
|
switch (textRun) |
||||
|
{ |
||||
|
case TextCharacters textCharacters: |
||||
|
{ |
||||
|
var shapeableRuns = textCharacters.GetShapeableCharacters(); |
||||
|
|
||||
|
foreach (var run in shapeableRuns) |
||||
|
{ |
||||
|
var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, |
||||
|
run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); |
||||
|
|
||||
|
var shapedCharacters = new ShapedTextCharacters(glyphRun, textRun.Properties); |
||||
|
|
||||
|
textRuns.Add(shapedCharacters); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (TryGetLineBreak(textRun, out var runLineBreak)) |
||||
|
{ |
||||
|
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); |
||||
|
|
||||
|
nextLineBreak = new TextLineBreak(splitResult.Second); |
||||
|
|
||||
|
return splitResult.First; |
||||
|
} |
||||
|
|
||||
|
currentLength += textRun.Text.Length; |
||||
|
} |
||||
|
|
||||
|
return textRuns; |
||||
|
} |
||||
|
|
||||
|
private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) |
||||
|
{ |
||||
|
lineBreak = default; |
||||
|
|
||||
|
if (textRun.Text.IsEmpty) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); |
||||
|
|
||||
|
while (lineBreakEnumerator.MoveNext()) |
||||
|
{ |
||||
|
if (!lineBreakEnumerator.Current.Required) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
lineBreak = lineBreakEnumerator.Current; |
||||
|
|
||||
|
if (lineBreak.PositionWrap >= textRun.Text.Length) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
//The line breaker isn't treating \n\r as a pair so we have to fix that here.
|
||||
|
if (textRun.Text[lineBreak.PositionMeasure] == '\n' |
||||
|
&& textRun.Text[lineBreak.PositionWrap] == '\r') |
||||
|
{ |
||||
|
lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, |
||||
|
lineBreak.Required); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs text trimming and returns a trimmed line.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text runs to perform the trimming on.</param>
|
||||
|
/// <param name="textRange">The text range that is covered by the text runs.</param>
|
||||
|
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
|
||||
|
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
|
||||
|
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange, |
||||
|
double paragraphWidth, TextParagraphProperties paragraphProperties) |
||||
|
{ |
||||
|
var textTrimming = paragraphProperties.TextTrimming; |
||||
|
var availableWidth = paragraphWidth; |
||||
|
var currentWidth = 0.0; |
||||
|
var runIndex = 0; |
||||
|
|
||||
|
while (runIndex < textRuns.Count) |
||||
|
{ |
||||
|
var currentRun = textRuns[runIndex]; |
||||
|
|
||||
|
currentWidth += currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
if (currentWidth > availableWidth) |
||||
|
{ |
||||
|
var ellipsisRun = CreateEllipsisRun(currentRun.Properties); |
||||
|
|
||||
|
var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); |
||||
|
|
||||
|
if (textTrimming == TextTrimming.WordEllipsis) |
||||
|
{ |
||||
|
if (measuredLength < textRange.End) |
||||
|
{ |
||||
|
var currentBreakPosition = 0; |
||||
|
|
||||
|
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
||||
|
|
||||
|
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
||||
|
|
||||
|
if (nextBreakPosition == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (nextBreakPosition > measuredLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
currentBreakPosition = nextBreakPosition; |
||||
|
} |
||||
|
|
||||
|
measuredLength = currentBreakPosition; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var splitResult = SplitTextRuns(textRuns, measuredLength); |
||||
|
|
||||
|
var trimmedRuns = new List<ShapedTextCharacters>(splitResult.First.Count + 1); |
||||
|
|
||||
|
trimmedRuns.AddRange(splitResult.First); |
||||
|
|
||||
|
trimmedRuns.Add(ellipsisRun); |
||||
|
|
||||
|
var textLineMetrics = |
||||
|
TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties); |
||||
|
|
||||
|
return new TextLineImpl(trimmedRuns, textLineMetrics); |
||||
|
} |
||||
|
|
||||
|
availableWidth -= currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return new TextLineImpl(textRuns, |
||||
|
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs text wrapping returns a list of text lines.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text run's.</param>
|
||||
|
/// <param name="textRange">The text range that is covered by the text runs.</param>
|
||||
|
/// <param name="paragraphWidth">The paragraph width.</param>
|
||||
|
/// <param name="paragraphProperties">The text paragraph properties.</param>
|
||||
|
/// <returns>The wrapped text line.</returns>
|
||||
|
private static TextLine PerformTextWrapping(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange, |
||||
|
double paragraphWidth, TextParagraphProperties paragraphProperties) |
||||
|
{ |
||||
|
var availableWidth = paragraphWidth; |
||||
|
var currentWidth = 0.0; |
||||
|
var runIndex = 0; |
||||
|
var length = 0; |
||||
|
|
||||
|
while (runIndex < textRuns.Count) |
||||
|
{ |
||||
|
var currentRun = textRuns[runIndex]; |
||||
|
|
||||
|
if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) |
||||
|
{ |
||||
|
var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); |
||||
|
|
||||
|
if (measuredLength < currentRun.Text.Length) |
||||
|
{ |
||||
|
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) |
||||
|
{ |
||||
|
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength)); |
||||
|
|
||||
|
if (lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
measuredLength += lineBreaker.Current.PositionWrap; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
measuredLength = currentRun.Text.Length; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var currentBreakPosition = -1; |
||||
|
|
||||
|
var lineBreaker = new LineBreakEnumerator(currentRun.Text); |
||||
|
|
||||
|
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
var nextBreakPosition = lineBreaker.Current.PositionWrap; |
||||
|
|
||||
|
if (nextBreakPosition == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (nextBreakPosition > measuredLength) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
currentBreakPosition = nextBreakPosition; |
||||
|
} |
||||
|
|
||||
|
if (currentBreakPosition != -1) |
||||
|
{ |
||||
|
measuredLength = currentBreakPosition; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
length += measuredLength; |
||||
|
|
||||
|
var splitResult = SplitTextRuns(textRuns, length); |
||||
|
|
||||
|
var textLineMetrics = TextLineMetrics.Create(splitResult.First, |
||||
|
new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties); |
||||
|
|
||||
|
var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? |
||||
|
new TextLineBreak(splitResult.Second) : |
||||
|
null; |
||||
|
|
||||
|
return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); |
||||
|
} |
||||
|
|
||||
|
currentWidth += currentRun.GlyphRun.Bounds.Width; |
||||
|
|
||||
|
length += currentRun.GlyphRun.Characters.Length; |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return new TextLineImpl(textRuns, |
||||
|
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Measures the number of characters that fits into available width.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textCharacters">The text run.</param>
|
||||
|
/// <param name="availableWidth">The available width.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth) |
||||
|
{ |
||||
|
var glyphRun = textCharacters.GlyphRun; |
||||
|
|
||||
|
var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); |
||||
|
|
||||
|
return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an ellipsis.
|
||||
|
/// </summary>
|
||||
|
/// <param name="properties">The text run properties.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties) |
||||
|
{ |
||||
|
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>(); |
||||
|
|
||||
|
var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize, |
||||
|
properties.CultureInfo); |
||||
|
|
||||
|
return new ShapedTextCharacters(glyphRun, properties); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the text range that is covered by the text runs.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text runs.</param>
|
||||
|
/// <returns>The text range that is covered by the text runs.</returns>
|
||||
|
private static TextRange GetTextRange(IReadOnlyList<TextRun> textRuns) |
||||
|
{ |
||||
|
if (textRuns is null || textRuns.Count == 0) |
||||
|
{ |
||||
|
return new TextRange(); |
||||
|
} |
||||
|
|
||||
|
var firstTextRun = textRuns[0]; |
||||
|
|
||||
|
if (textRuns.Count == 1) |
||||
|
{ |
||||
|
return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length); |
||||
|
} |
||||
|
|
||||
|
var start = firstTextRun.Text.Start; |
||||
|
|
||||
|
var end = textRuns[textRuns.Count - 1].Text.End + 1; |
||||
|
|
||||
|
return new TextRange(start, end - start); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Split a sequence of runs into two segments at specified length.
|
||||
|
/// </summary>
|
||||
|
/// <param name="textRuns">The text run's.</param>
|
||||
|
/// <param name="length">The length to split at.</param>
|
||||
|
/// <returns>The split text runs.</returns>
|
||||
|
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length) |
||||
|
{ |
||||
|
var currentLength = 0; |
||||
|
|
||||
|
for (var i = 0; i < textRuns.Count; i++) |
||||
|
{ |
||||
|
var currentRun = textRuns[i]; |
||||
|
|
||||
|
if (currentLength + currentRun.GlyphRun.Characters.Length < length) |
||||
|
{ |
||||
|
currentLength += currentRun.GlyphRun.Characters.Length; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; |
||||
|
|
||||
|
var first = new ShapedTextCharacters[firstCount]; |
||||
|
|
||||
|
if (firstCount > 1) |
||||
|
{ |
||||
|
for (var j = 0; j < i; j++) |
||||
|
{ |
||||
|
first[j] = textRuns[j]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var secondCount = textRuns.Count - firstCount; |
||||
|
|
||||
|
if (currentLength + currentRun.GlyphRun.Characters.Length == length) |
||||
|
{ |
||||
|
var second = new ShapedTextCharacters[secondCount]; |
||||
|
|
||||
|
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; |
||||
|
|
||||
|
if (secondCount > 0) |
||||
|
{ |
||||
|
for (var j = 0; j < secondCount; j++) |
||||
|
{ |
||||
|
second[j] = textRuns[i + j + offset]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
first[i] = currentRun; |
||||
|
|
||||
|
return new SplitTextRunsResult(first, second); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
secondCount++; |
||||
|
|
||||
|
var second = new ShapedTextCharacters[secondCount]; |
||||
|
|
||||
|
if (secondCount > 0) |
||||
|
{ |
||||
|
for (var j = 1; j < secondCount; j++) |
||||
|
{ |
||||
|
second[j] = textRuns[i + j]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var split = currentRun.Split(length - currentLength); |
||||
|
|
||||
|
first[i] = split.First; |
||||
|
|
||||
|
second[0] = split.Second; |
||||
|
|
||||
|
return new SplitTextRunsResult(first, second); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new SplitTextRunsResult(textRuns, null); |
||||
|
} |
||||
|
|
||||
|
private readonly struct SplitTextRunsResult |
||||
|
{ |
||||
|
public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second) |
||||
|
{ |
||||
|
First = first; |
||||
|
|
||||
|
Second = second; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the first text runs.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The first text runs.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<ShapedTextCharacters> First { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the second text runs.
|
||||
|
/// </summary>
|
||||
|
/// <value>
|
||||
|
/// The second text runs.
|
||||
|
/// </value>
|
||||
|
public IReadOnlyList<ShapedTextCharacters> Second { get; } |
||||
|
} |
||||
|
|
||||
|
private struct TextRunEnumerator |
||||
|
{ |
||||
|
private readonly ITextSource _textSource; |
||||
|
private int _pos; |
||||
|
|
||||
|
public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex) |
||||
|
{ |
||||
|
_textSource = textSource; |
||||
|
_pos = firstTextSourceIndex; |
||||
|
Current = null; |
||||
|
} |
||||
|
|
||||
|
// ReSharper disable once MemberHidesStaticFromOuterClass
|
||||
|
public TextRun Current { get; private set; } |
||||
|
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
Current = _textSource.GetTextRun(_pos); |
||||
|
|
||||
|
if (Current is null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (Current.TextSourceLength == 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
_pos += Current.TextSourceLength; |
||||
|
|
||||
|
return !(Current is TextEndOfLine); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
public class TextLineBreak |
||||
|
{ |
||||
|
public TextLineBreak(IReadOnlyList<ShapedTextCharacters> remainingCharacters) |
||||
|
{ |
||||
|
RemainingCharacters = remainingCharacters; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the remaining shaped characters that were split up by the <see cref="TextFormatter"/> during the formatting process.
|
||||
|
/// </summary>
|
||||
|
public IReadOnlyList<ShapedTextCharacters> RemainingCharacters { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,235 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
internal class TextLineImpl : TextLine |
||||
|
{ |
||||
|
private readonly IReadOnlyList<ShapedTextCharacters> _textRuns; |
||||
|
|
||||
|
public TextLineImpl(IReadOnlyList<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics, |
||||
|
TextLineBreak lineBreak = null) |
||||
|
{ |
||||
|
_textRuns = textRuns; |
||||
|
LineMetrics = lineMetrics; |
||||
|
LineBreak = lineBreak; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override TextRange TextRange => LineMetrics.TextRange; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override IReadOnlyList<TextRun> TextRuns => _textRuns; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override TextLineMetrics LineMetrics { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override TextLineBreak LineBreak { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Draw(DrawingContext drawingContext, Point origin) |
||||
|
{ |
||||
|
var currentX = origin.X; |
||||
|
|
||||
|
foreach (var textRun in _textRuns) |
||||
|
{ |
||||
|
var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, |
||||
|
origin.Y + LineMetrics.BaselineOrigin.Y); |
||||
|
|
||||
|
textRun.Draw(drawingContext, baselineOrigin); |
||||
|
|
||||
|
currentX += textRun.Bounds.Width; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override CharacterHit GetCharacterHitFromDistance(double distance) |
||||
|
{ |
||||
|
if (distance < 0) |
||||
|
{ |
||||
|
// hit happens before the line, return the first position
|
||||
|
return new CharacterHit(TextRange.Start); |
||||
|
} |
||||
|
|
||||
|
// process hit that happens within the line
|
||||
|
var characterHit = new CharacterHit(); |
||||
|
|
||||
|
foreach (var run in _textRuns) |
||||
|
{ |
||||
|
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); |
||||
|
|
||||
|
if (distance <= run.Bounds.Width) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
distance -= run.Bounds.Width; |
||||
|
} |
||||
|
|
||||
|
return characterHit; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) |
||||
|
{ |
||||
|
return nextCharacterHit; |
||||
|
} |
||||
|
|
||||
|
return new CharacterHit(TextRange.End); // Can't move, we're after the last character
|
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) |
||||
|
{ |
||||
|
return previousCharacterHit; |
||||
|
} |
||||
|
|
||||
|
return new CharacterHit(TextRange.Start); // Can't move, we're before the first character
|
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) |
||||
|
{ |
||||
|
// same operation as move-to-previous
|
||||
|
return GetPreviousCaretCharacterHit(characterHit); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get distance from line start to the specified codepoint index.
|
||||
|
/// </summary>
|
||||
|
private double DistanceFromCodepointIndex(int codepointIndex) |
||||
|
{ |
||||
|
var currentDistance = 0.0; |
||||
|
|
||||
|
foreach (var textRun in _textRuns) |
||||
|
{ |
||||
|
if (codepointIndex > textRun.Text.End) |
||||
|
{ |
||||
|
currentDistance += textRun.Bounds.Width; |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); |
||||
|
} |
||||
|
|
||||
|
return currentDistance; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to find the next character hit.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">The current character hit.</param>
|
||||
|
/// <param name="nextCharacterHit">The next character hit.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit) |
||||
|
{ |
||||
|
nextCharacterHit = characterHit; |
||||
|
|
||||
|
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; |
||||
|
|
||||
|
if (codepointIndex >= TextRange.Start + TextRange.Length) |
||||
|
{ |
||||
|
return false; // Cannot go forward anymore
|
||||
|
} |
||||
|
|
||||
|
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); |
||||
|
|
||||
|
while (runIndex < TextRuns.Count) |
||||
|
{ |
||||
|
var run = _textRuns[runIndex]; |
||||
|
|
||||
|
nextCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); |
||||
|
|
||||
|
if (codepointIndex <= nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tries to find the previous character hit.
|
||||
|
/// </summary>
|
||||
|
/// <param name="characterHit">The current character hit.</param>
|
||||
|
/// <param name="previousCharacterHit">The previous character hit.</param>
|
||||
|
/// <returns></returns>
|
||||
|
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) |
||||
|
{ |
||||
|
previousCharacterHit = characterHit; |
||||
|
|
||||
|
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; |
||||
|
|
||||
|
if (codepointIndex < TextRange.Start) |
||||
|
{ |
||||
|
return false; // Cannot go backward anymore.
|
||||
|
} |
||||
|
|
||||
|
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); |
||||
|
|
||||
|
while (runIndex >= 0) |
||||
|
{ |
||||
|
var run = _textRuns[runIndex]; |
||||
|
|
||||
|
previousCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); |
||||
|
|
||||
|
if (previousCharacterHit.FirstCharacterIndex < codepointIndex) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
runIndex--; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the run index of the specified codepoint index.
|
||||
|
/// </summary>
|
||||
|
/// <param name="codepointIndex">The codepoint index.</param>
|
||||
|
/// <returns>The text run index.</returns>
|
||||
|
private int GetRunIndexAtCodepointIndex(int codepointIndex) |
||||
|
{ |
||||
|
if (codepointIndex >= TextRange.End) |
||||
|
{ |
||||
|
return _textRuns.Count - 1; |
||||
|
} |
||||
|
|
||||
|
if (codepointIndex <= 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
var runIndex = 0; |
||||
|
|
||||
|
while (runIndex < _textRuns.Count) |
||||
|
{ |
||||
|
var run = _textRuns[runIndex]; |
||||
|
|
||||
|
if (run.Text.End > codepointIndex) |
||||
|
{ |
||||
|
return runIndex; |
||||
|
} |
||||
|
|
||||
|
runIndex++; |
||||
|
} |
||||
|
|
||||
|
return runIndex; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Properties that can change from one run to the next, such as typeface or foreground brush.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The client provides a concrete implementation of this abstract run properties class. This
|
||||
|
/// allows client to implement their run properties the way that fits with their run formatting
|
||||
|
/// store.
|
||||
|
/// </remarks>
|
||||
|
public abstract class TextRunProperties : IEquatable<TextRunProperties> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Run typeface
|
||||
|
/// </summary>
|
||||
|
public abstract Typeface Typeface { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Em size of font used to format and display text
|
||||
|
/// </summary>
|
||||
|
public abstract double FontRenderingEmSize { get; } |
||||
|
|
||||
|
///<summary>
|
||||
|
/// Run TextDecorations.
|
||||
|
///</summary>
|
||||
|
public abstract TextDecorationCollection TextDecorations { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Brush used to fill text.
|
||||
|
/// </summary>
|
||||
|
public abstract IBrush ForegroundBrush { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Brush used to paint background of run.
|
||||
|
/// </summary>
|
||||
|
public abstract IBrush BackgroundBrush { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Run text culture.
|
||||
|
/// </summary>
|
||||
|
public abstract CultureInfo CultureInfo { get; } |
||||
|
|
||||
|
public bool Equals(TextRunProperties other) |
||||
|
{ |
||||
|
if (ReferenceEquals(null, other)) |
||||
|
return false; |
||||
|
if (ReferenceEquals(this, other)) |
||||
|
return true; |
||||
|
|
||||
|
return Typeface.Equals(other.Typeface) && |
||||
|
FontRenderingEmSize.Equals(other.FontRenderingEmSize) |
||||
|
&& Equals(TextDecorations, other.TextDecorations) && |
||||
|
Equals(ForegroundBrush, other.ForegroundBrush) && |
||||
|
Equals(BackgroundBrush, other.BackgroundBrush) && |
||||
|
Equals(CultureInfo, other.CultureInfo); |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
return ReferenceEquals(this, obj) || obj is TextRunProperties other && Equals(other); |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
unchecked |
||||
|
{ |
||||
|
var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); |
||||
|
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); |
||||
|
hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0); |
||||
|
hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0); |
||||
|
hashCode = (hashCode * 397) ^ (BackgroundBrush != null ? BackgroundBrush.GetHashCode() : 0); |
||||
|
hashCode = (hashCode * 397) ^ (CultureInfo != null ? CultureInfo.GetHashCode() : 0); |
||||
|
return hashCode; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static bool operator ==(TextRunProperties left, TextRunProperties right) |
||||
|
{ |
||||
|
return Equals(left, right); |
||||
|
} |
||||
|
|
||||
|
public static bool operator !=(TextRunProperties left, TextRunProperties right) |
||||
|
{ |
||||
|
return !Equals(left, right); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,39 +0,0 @@ |
|||||
using Avalonia.Media.Immutable; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Unique text formatting properties that effect the styling of a text.
|
|
||||
/// </summary>
|
|
||||
public readonly struct TextStyle |
|
||||
{ |
|
||||
public TextStyle(Typeface typeface, double fontRenderingEmSize = 12, IBrush foreground = null, |
|
||||
ImmutableTextDecoration[] textDecorations = null) |
|
||||
: this(new TextFormat(typeface, fontRenderingEmSize), foreground, textDecorations) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public TextStyle(TextFormat textFormat, IBrush foreground = null, |
|
||||
ImmutableTextDecoration[] textDecorations = null) |
|
||||
{ |
|
||||
TextFormat = textFormat; |
|
||||
Foreground = foreground; |
|
||||
TextDecorations = textDecorations; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text format.
|
|
||||
/// </summary>
|
|
||||
public TextFormat TextFormat { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the foreground.
|
|
||||
/// </summary>
|
|
||||
public IBrush Foreground { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text decorations.
|
|
||||
/// </summary>
|
|
||||
public ImmutableTextDecoration[] TextDecorations { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a text run's style and is used during the layout process of the <see cref="TextFormatter"/>.
|
|
||||
/// </summary>
|
|
||||
public readonly struct TextStyleRun |
|
||||
{ |
|
||||
public TextStyleRun(TextPointer textPointer, TextStyle style) |
|
||||
{ |
|
||||
TextPointer = textPointer; |
|
||||
Style = style; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text pointer.
|
|
||||
/// </summary>
|
|
||||
public TextPointer TextPointer { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the text style.
|
|
||||
/// </summary>
|
|
||||
public TextStyle Style { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,30 @@ |
|||||
|
namespace Avalonia.Utilities |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Pairing of value and positions sharing that value.
|
||||
|
/// </summary>
|
||||
|
public readonly struct ValueSpan<T> |
||||
|
{ |
||||
|
public ValueSpan(int start, int length, T value) |
||||
|
{ |
||||
|
Start = start; |
||||
|
Length = length; |
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get's the start of the span.
|
||||
|
/// </summary>
|
||||
|
public int Start { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get's the length of the span.
|
||||
|
/// </summary>
|
||||
|
public int Length { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get's the value of the span.
|
||||
|
/// </summary>
|
||||
|
public T Value { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
internal class FormattableTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly TextRunProperties _defaultStyle; |
||||
|
private ReadOnlySlice<ValueSpan<TextRunProperties>> _styleSpans; |
||||
|
|
||||
|
public FormattableTextSource(string text, TextRunProperties defaultStyle, |
||||
|
ReadOnlySlice<ValueSpan<TextRunProperties>> styleSpans) |
||||
|
{ |
||||
|
_text = text.AsMemory(); |
||||
|
|
||||
|
_defaultStyle = defaultStyle; |
||||
|
|
||||
|
_styleSpans = styleSpans; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
if (_styleSpans.IsEmpty) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
var currentSpan = _styleSpans[0]; |
||||
|
|
||||
|
_styleSpans = _styleSpans.Skip(1); |
||||
|
|
||||
|
return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length), |
||||
|
_defaultStyle); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
internal class MultiBufferTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly string[] _runTexts; |
||||
|
private readonly GenericTextRunProperties _defaultStyle; |
||||
|
|
||||
|
public MultiBufferTextSource(GenericTextRunProperties defaultStyle) |
||||
|
{ |
||||
|
_defaultStyle = defaultStyle; |
||||
|
|
||||
|
_runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; |
||||
|
} |
||||
|
|
||||
|
public static TextRange TextRange => new TextRange(0, 50); |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
if (textSourceIndex == 50) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
var index = textSourceIndex / 10; |
||||
|
|
||||
|
var runText = _runTexts[index]; |
||||
|
|
||||
|
return new TextCharacters( |
||||
|
new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
internal class SingleBufferTextSource : ITextSource |
||||
|
{ |
||||
|
private readonly ReadOnlySlice<char> _text; |
||||
|
private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; |
||||
|
|
||||
|
public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) |
||||
|
{ |
||||
|
_text = text.AsMemory(); |
||||
|
_defaultGenericPropertiesRunProperties = defaultProperties; |
||||
|
} |
||||
|
|
||||
|
public TextRun GetTextRun(int textSourceIndex) |
||||
|
{ |
||||
|
var runText = _text.Skip(textSourceIndex); |
||||
|
|
||||
|
if (runText.IsEmpty) |
||||
|
{ |
||||
|
return new TextEndOfParagraph(); |
||||
|
} |
||||
|
|
||||
|
return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,275 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Media.TextFormatting.Unicode; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.Utilities; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
public class TextFormatterTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_Default_Style() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var defaultProperties = |
||||
|
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
Assert.Single(textLine.TextRuns); |
||||
|
|
||||
|
var textRun = textLine.TextRuns[0]; |
||||
|
|
||||
|
Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface); |
||||
|
|
||||
|
Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); |
||||
|
|
||||
|
Assert.Equal(text.Length, textRun.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_Multiple_Buffers() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = |
||||
|
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); |
||||
|
|
||||
|
var textSource = new MultiBufferTextSource(defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
Assert.Equal(5, textLine.TextRuns.Count); |
||||
|
|
||||
|
Assert.Equal(50, textLine.TextRange.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Format_TextRuns_With_TextRunStyles() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "0123456789"; |
||||
|
|
||||
|
var defaultProperties = |
||||
|
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); |
||||
|
|
||||
|
var GenericTextRunPropertiesRuns = new[] |
||||
|
{ |
||||
|
new ValueSpan<TextRunProperties>(0, 3, defaultProperties), |
||||
|
new ValueSpan<TextRunProperties>(3, 3, |
||||
|
new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)), |
||||
|
new ValueSpan<TextRunProperties>(6, 3, |
||||
|
new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)), |
||||
|
new ValueSpan<TextRunProperties>(9, 1, defaultProperties) |
||||
|
}; |
||||
|
|
||||
|
var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
Assert.Equal(text.Length, textLine.TextRange.Length); |
||||
|
|
||||
|
for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++) |
||||
|
{ |
||||
|
var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i]; |
||||
|
|
||||
|
var textRun = textLine.TextRuns[i]; |
||||
|
|
||||
|
Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("0123", 1)] |
||||
|
[InlineData("\r\n", 1)] |
||||
|
[InlineData("👍b", 2)] |
||||
|
[InlineData("a👍b", 3)] |
||||
|
[InlineData("a👍子b", 4)] |
||||
|
public void Should_Produce_Unique_Runs(string text, int numberOfRuns) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
Assert.Equal(numberOfRuns, textLine.TextRuns.Count); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Split_Run_On_Script() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "1234الدولي"; |
||||
|
|
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
Assert.Equal(4, textLine.TextRuns[0].Text.Length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)] |
||||
|
[InlineData("01234 56789 01234 56789", 6, 4)] |
||||
|
[Theory] |
||||
|
public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var numberOfLines = 0; |
||||
|
|
||||
|
var currentPosition = 0; |
||||
|
|
||||
|
while (currentPosition < text.Length) |
||||
|
{ |
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, currentPosition, 1, |
||||
|
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow)); |
||||
|
|
||||
|
if (text.Length - currentPosition > expectedCharactersPerLine) |
||||
|
{ |
||||
|
Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length); |
||||
|
} |
||||
|
|
||||
|
currentPosition += textLine.TextRange.Length; |
||||
|
|
||||
|
numberOfLines++; |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(expectedNumberOfLines, numberOfLines); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + |
||||
|
"IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." |
||||
|
, "Noto Sans", 40)] |
||||
|
[InlineData("01234 56789 01234 56789", "Noto Mono", 7)] |
||||
|
[Theory] |
||||
|
public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var lineBreaker = new LineBreakEnumerator(text.AsMemory()); |
||||
|
|
||||
|
var expected = new List<int>(); |
||||
|
|
||||
|
while (lineBreaker.MoveNext()) |
||||
|
{ |
||||
|
expected.Add(lineBreaker.Current.PositionWrap - 1); |
||||
|
} |
||||
|
|
||||
|
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + |
||||
|
familyName); |
||||
|
|
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var glyph = typeface.GlyphTypeface.GetGlyph('a'); |
||||
|
|
||||
|
var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * |
||||
|
(12.0 / typeface.GlyphTypeface.DesignEmHeight); |
||||
|
|
||||
|
var paragraphWidth = advance * numberOfCharactersPerLine; |
||||
|
|
||||
|
var currentPosition = 0; |
||||
|
|
||||
|
while (currentPosition < text.Length) |
||||
|
{ |
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, currentPosition, paragraphWidth, |
||||
|
new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap)); |
||||
|
|
||||
|
Assert.True(expected.Contains(textLine.TextRange.End)); |
||||
|
|
||||
|
var index = expected.IndexOf(textLine.TextRange.End); |
||||
|
|
||||
|
for (var i = 0; i <= index; i++) |
||||
|
{ |
||||
|
expected.RemoveAt(0); |
||||
|
} |
||||
|
|
||||
|
currentPosition += textLine.TextRange.Length; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Produce_Fixed_Height_Lines() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
const string text = "012345"; |
||||
|
|
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties, lineHeight: 50)); |
||||
|
|
||||
|
Assert.Equal(50, textLine.LineMetrics.Size.Height); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static IDisposable Start() |
||||
|
{ |
||||
|
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface |
||||
|
.With(renderInterface: new PlatformRenderInterface(null), |
||||
|
textShaperImpl: new TextShaperImpl())); |
||||
|
|
||||
|
AvaloniaLocator.CurrentMutable |
||||
|
.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl())); |
||||
|
|
||||
|
return disposable; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,175 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
public class TextLineTests |
||||
|
{ |
||||
|
[InlineData("𐐷𐐷𐐷𐐷𐐷")] |
||||
|
[InlineData("𐐷1234")] |
||||
|
[Theory] |
||||
|
public void Should_Get_Next_Caret_CharacterHit(string text) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
var clusters = textLine.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.GlyphRun.GlyphClusters) |
||||
|
.ToArray(); |
||||
|
|
||||
|
var nextCharacterHit = new CharacterHit(0); |
||||
|
|
||||
|
for (var i = 1; i < clusters.Length; i++) |
||||
|
{ |
||||
|
nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); |
||||
|
|
||||
|
Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[InlineData("𐐷𐐷𐐷𐐷𐐷")] |
||||
|
[InlineData("𐐷1234")] |
||||
|
[Theory] |
||||
|
public void Should_Get_Previous_Caret_CharacterHit(string text) |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new SingleBufferTextSource(text, defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
var clusters = textLine.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.GlyphRun.GlyphClusters) |
||||
|
.ToArray(); |
||||
|
|
||||
|
var previousCharacterHit = new CharacterHit(clusters[^1]); |
||||
|
|
||||
|
for (var i = clusters.Length - 2; i > 0; i--) |
||||
|
{ |
||||
|
previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit); |
||||
|
|
||||
|
Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Get_Distance_From_CharacterHit() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new MultiBufferTextSource(defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
var currentDistance = 0.0; |
||||
|
|
||||
|
foreach (var run in textLine.TextRuns) |
||||
|
{ |
||||
|
var textRun = (ShapedTextCharacters)run; |
||||
|
|
||||
|
var glyphRun = textRun.GlyphRun; |
||||
|
|
||||
|
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) |
||||
|
{ |
||||
|
var cluster = glyphRun.GlyphClusters[i]; |
||||
|
|
||||
|
var glyph = glyphRun.GlyphIndices[i]; |
||||
|
|
||||
|
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; |
||||
|
|
||||
|
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); |
||||
|
|
||||
|
Assert.Equal(currentDistance, distance); |
||||
|
|
||||
|
currentDistance += advance; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(currentDistance, |
||||
|
textLine.GetDistanceFromCharacterHit(new CharacterHit(MultiBufferTextSource.TextRange.Length))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_Get_CharacterHit_From_Distance() |
||||
|
{ |
||||
|
using (Start()) |
||||
|
{ |
||||
|
var defaultProperties = new GenericTextRunProperties(Typeface.Default); |
||||
|
|
||||
|
var textSource = new MultiBufferTextSource(defaultProperties); |
||||
|
|
||||
|
var formatter = new TextFormatterImpl(); |
||||
|
|
||||
|
var textLine = |
||||
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
||||
|
new GenericTextParagraphProperties(defaultProperties)); |
||||
|
|
||||
|
var currentDistance = 0.0; |
||||
|
|
||||
|
CharacterHit characterHit; |
||||
|
|
||||
|
foreach (var run in textLine.TextRuns) |
||||
|
{ |
||||
|
var textRun = (ShapedTextCharacters)run; |
||||
|
|
||||
|
var glyphRun = textRun.GlyphRun; |
||||
|
|
||||
|
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) |
||||
|
{ |
||||
|
var cluster = glyphRun.GlyphClusters[i]; |
||||
|
|
||||
|
var glyph = glyphRun.GlyphIndices[i]; |
||||
|
|
||||
|
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; |
||||
|
|
||||
|
characterHit = textLine.GetCharacterHitFromDistance(currentDistance); |
||||
|
|
||||
|
Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); |
||||
|
|
||||
|
currentDistance += advance; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); |
||||
|
|
||||
|
Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static IDisposable Start() |
||||
|
{ |
||||
|
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface |
||||
|
.With(renderInterface: new PlatformRenderInterface(null), |
||||
|
textShaperImpl: new TextShaperImpl(), |
||||
|
fontManagerImpl: new CustomFontManagerImpl())); |
||||
|
|
||||
|
return disposable; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,373 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Media.TextFormatting; |
|
||||
using Avalonia.Media.TextFormatting.Unicode; |
|
||||
using Avalonia.UnitTests; |
|
||||
using Avalonia.Utility; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Skia.UnitTests |
|
||||
{ |
|
||||
public class SimpleTextFormatterTests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Should_Format_TextRuns_With_Default_Style() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
const string text = "0123456789"; |
|
||||
|
|
||||
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
|
||||
|
|
||||
var textSource = new SimpleTextSource(text, defaultTextRunStyle); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
Assert.Single(textLine.TextRuns); |
|
||||
|
|
||||
var textRun = textLine.TextRuns[0]; |
|
||||
|
|
||||
Assert.Equal(defaultTextRunStyle.TextFormat, textRun.Style.TextFormat); |
|
||||
|
|
||||
Assert.Equal(defaultTextRunStyle.Foreground, textRun.Style.Foreground); |
|
||||
|
|
||||
Assert.Equal(text.Length, textRun.Text.Length); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Format_TextRuns_With_Multiple_Buffers() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
|
||||
|
|
||||
var textSource = new MultiBufferTextSource(defaultTextRunStyle); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, |
|
||||
new TextParagraphProperties(defaultTextRunStyle)); |
|
||||
|
|
||||
Assert.Equal(5, textLine.TextRuns.Count); |
|
||||
|
|
||||
Assert.Equal(50, textLine.Text.Length); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class MultiBufferTextSource : ITextSource |
|
||||
{ |
|
||||
private readonly string[] _runTexts; |
|
||||
private readonly TextStyle _defaultStyle; |
|
||||
|
|
||||
public MultiBufferTextSource(TextStyle defaultStyle) |
|
||||
{ |
|
||||
_defaultStyle = defaultStyle; |
|
||||
|
|
||||
_runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; |
|
||||
} |
|
||||
|
|
||||
public TextPointer TextPointer => new TextPointer(0, 50); |
|
||||
|
|
||||
public TextRun GetTextRun(int textSourceIndex) |
|
||||
{ |
|
||||
if (textSourceIndex == 50) |
|
||||
{ |
|
||||
return new TextEndOfParagraph(); |
|
||||
} |
|
||||
|
|
||||
var index = textSourceIndex / 10; |
|
||||
|
|
||||
var runText = _runTexts[index]; |
|
||||
|
|
||||
return new TextCharacters( |
|
||||
new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Format_TextRuns_With_TextRunStyles() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
const string text = "0123456789"; |
|
||||
|
|
||||
var defaultStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); |
|
||||
|
|
||||
var textStyleRuns = new[] |
|
||||
{ |
|
||||
new TextStyleRun(new TextPointer(0, 3), defaultStyle ), |
|
||||
new TextStyleRun(new TextPointer(3, 3), new TextStyle(Typeface.Default, 13, Brushes.Black) ), |
|
||||
new TextStyleRun(new TextPointer(6, 3), new TextStyle(Typeface.Default, 14, Brushes.Black) ), |
|
||||
new TextStyleRun(new TextPointer(9, 1), defaultStyle ) |
|
||||
}; |
|
||||
|
|
||||
var textSource = new FormattableTextSource(text, defaultStyle, textStyleRuns); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
Assert.Equal(text.Length, textLine.Text.Length); |
|
||||
|
|
||||
for (var i = 0; i < textStyleRuns.Length; i++) |
|
||||
{ |
|
||||
var textStyleRun = textStyleRuns[i]; |
|
||||
|
|
||||
var textRun = textLine.TextRuns[i]; |
|
||||
|
|
||||
Assert.Equal(textStyleRun.TextPointer.Length, textRun.Text.Length); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class FormattableTextSource : ITextSource |
|
||||
{ |
|
||||
private readonly ReadOnlySlice<char> _text; |
|
||||
private readonly TextStyle _defaultStyle; |
|
||||
private ReadOnlySlice<TextStyleRun> _textStyleRuns; |
|
||||
|
|
||||
public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice<TextStyleRun> textStyleRuns) |
|
||||
{ |
|
||||
_text = text.AsMemory(); |
|
||||
|
|
||||
_defaultStyle = defaultStyle; |
|
||||
|
|
||||
_textStyleRuns = textStyleRuns; |
|
||||
} |
|
||||
|
|
||||
public TextRun GetTextRun(int textSourceIndex) |
|
||||
{ |
|
||||
if (_textStyleRuns.IsEmpty) |
|
||||
{ |
|
||||
return new TextEndOfParagraph(); |
|
||||
} |
|
||||
|
|
||||
var styleRun = _textStyleRuns[0]; |
|
||||
|
|
||||
_textStyleRuns = _textStyleRuns.Skip(1); |
|
||||
|
|
||||
return new TextCharacters(_text.AsSlice(styleRun.TextPointer.Start, styleRun.TextPointer.Length), |
|
||||
_defaultStyle); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("0123", 1)] |
|
||||
[InlineData("\r\n", 1)] |
|
||||
[InlineData("👍b", 2)] |
|
||||
[InlineData("a👍b", 3)] |
|
||||
[InlineData("a👍子b", 4)] |
|
||||
public void Should_Produce_Unique_Runs(string text, int numberOfRuns) |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = |
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
Assert.Equal(numberOfRuns, textLine.TextRuns.Count); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class SimpleTextSource : ITextSource |
|
||||
{ |
|
||||
private readonly ReadOnlySlice<char> _text; |
|
||||
private readonly TextStyle _defaultTextStyle; |
|
||||
|
|
||||
public SimpleTextSource(string text, TextStyle defaultText) |
|
||||
{ |
|
||||
_text = text.AsMemory(); |
|
||||
_defaultTextStyle = defaultText; |
|
||||
} |
|
||||
|
|
||||
public TextRun GetTextRun(int textSourceIndex) |
|
||||
{ |
|
||||
var runText = _text.Skip(textSourceIndex); |
|
||||
|
|
||||
if (runText.IsEmpty) |
|
||||
{ |
|
||||
return new TextEndOfParagraph(); |
|
||||
} |
|
||||
|
|
||||
return new TextCharacters(runText, _defaultTextStyle); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Split_Run_On_Script() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
const string text = "1234الدولي"; |
|
||||
|
|
||||
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = |
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
Assert.Equal(4, textLine.TextRuns[0].Text.Length); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Get_Distance_From_CharacterHit() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = |
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
var currentDistance = 0.0; |
|
||||
|
|
||||
foreach (var run in textLine.TextRuns) |
|
||||
{ |
|
||||
var textRun = (ShapedTextRun)run; |
|
||||
|
|
||||
var glyphRun = textRun.GlyphRun; |
|
||||
|
|
||||
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) |
|
||||
{ |
|
||||
var cluster = glyphRun.GlyphClusters[i]; |
|
||||
|
|
||||
var glyph = glyphRun.GlyphIndices[i]; |
|
||||
|
|
||||
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; |
|
||||
|
|
||||
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); |
|
||||
|
|
||||
Assert.Equal(currentDistance, distance); |
|
||||
|
|
||||
currentDistance += advance; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(textSource.TextPointer.Length))); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Get_CharacterHit_From_Distance() |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var textLine = |
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); |
|
||||
|
|
||||
var currentDistance = 0.0; |
|
||||
|
|
||||
CharacterHit characterHit; |
|
||||
|
|
||||
foreach (var run in textLine.TextRuns) |
|
||||
{ |
|
||||
var textRun = (ShapedTextRun)run; |
|
||||
|
|
||||
var glyphRun = textRun.GlyphRun; |
|
||||
|
|
||||
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) |
|
||||
{ |
|
||||
var cluster = glyphRun.GlyphClusters[i]; |
|
||||
|
|
||||
var glyph = glyphRun.GlyphIndices[i]; |
|
||||
|
|
||||
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; |
|
||||
|
|
||||
characterHit = textLine.GetCharacterHitFromDistance(currentDistance); |
|
||||
|
|
||||
Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); |
|
||||
|
|
||||
currentDistance += advance; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); |
|
||||
|
|
||||
Assert.Equal(textSource.TextPointer.End, characterHit.FirstCharacterIndex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + |
|
||||
"IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." |
|
||||
, "Noto Sans", 40)] |
|
||||
[InlineData("01234 56789 01234 56789", "Noto Mono", 7)] |
|
||||
[Theory] |
|
||||
public void Should_Wrap_Text(string text, string familyName, int numberOfCharactersPerLine) |
|
||||
{ |
|
||||
using (Start()) |
|
||||
{ |
|
||||
var lineBreaker = new LineBreakEnumerator(text.AsMemory()); |
|
||||
|
|
||||
var expected = new List<int>(); |
|
||||
|
|
||||
while (lineBreaker.MoveNext()) |
|
||||
{ |
|
||||
expected.Add(lineBreaker.Current.PositionWrap - 1); |
|
||||
} |
|
||||
|
|
||||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + |
|
||||
familyName); |
|
||||
|
|
||||
var defaultStyle = new TextStyle(typeface); |
|
||||
|
|
||||
var textSource = new SimpleTextSource(text, defaultStyle); |
|
||||
|
|
||||
var formatter = new SimpleTextFormatter(); |
|
||||
|
|
||||
var glyph = typeface.GlyphTypeface.GetGlyph('a'); |
|
||||
|
|
||||
var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * |
|
||||
(12.0 / typeface.GlyphTypeface.DesignEmHeight); |
|
||||
|
|
||||
var paragraphWidth = advance * numberOfCharactersPerLine; |
|
||||
|
|
||||
var currentPosition = 0; |
|
||||
|
|
||||
while (currentPosition < text.Length) |
|
||||
{ |
|
||||
var textLine = |
|
||||
formatter.FormatLine(textSource, currentPosition, paragraphWidth, |
|
||||
new TextParagraphProperties(defaultStyle, textWrapping: TextWrapping.Wrap)); |
|
||||
|
|
||||
Assert.True(expected.Contains(textLine.Text.End)); |
|
||||
|
|
||||
var index = expected.IndexOf(textLine.Text.End); |
|
||||
|
|
||||
for (var i = 0; i <= index; i++) |
|
||||
{ |
|
||||
expected.RemoveAt(0); |
|
||||
} |
|
||||
|
|
||||
currentPosition += textLine.Text.Length; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static IDisposable Start() |
|
||||
{ |
|
||||
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface |
|
||||
.With(renderInterface: new PlatformRenderInterface(null), |
|
||||
textShaperImpl: new TextShaperImpl())); |
|
||||
|
|
||||
AvaloniaLocator.CurrentMutable |
|
||||
.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl())); |
|
||||
|
|
||||
return disposable; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue