diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index f73ef9b4fb..4b8edcf98c 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -64,51 +64,42 @@
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="2">
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="1">
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ StrokeThicknessUnit="Pixel"
+ StrokeThickness="2">
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 4208a2f2f7..dd33023e38 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -110,7 +110,7 @@ namespace Avalonia.Controls.Primitives
foreach (var textLine in TextLayout.TextLines)
{
- if (textLine.Text.End < textPosition)
+ if (textLine.TextRange.End < textPosition)
{
currentY += textLine.LineMetrics.Size.Height;
@@ -121,21 +121,22 @@ namespace Avalonia.Controls.Primitives
foreach (var textRun in textLine.TextRuns)
{
- if (!(textRun is ShapedTextRun shapedRun))
+ if (!(textRun is ShapedTextCharacters shapedTextCharacters))
{
continue;
}
- if (shapedRun.GlyphRun.Characters.End < textPosition)
+ if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
{
- currentX += shapedRun.GlyphRun.Bounds.Width;
+ currentX += shapedTextCharacters.GlyphRun.Bounds.Width;
continue;
}
- var characterHit = shapedRun.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
+ var characterHit =
+ shapedTextCharacters.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
- var distance = shapedRun.GlyphRun.GetDistanceFromCharacterHit(characterHit);
+ var distance = shapedTextCharacters.GlyphRun.GetDistanceFromCharacterHit(characterHit);
currentX += distance - width;
@@ -144,7 +145,7 @@ namespace Avalonia.Controls.Primitives
width = 0.0;
}
- return new Rect(currentX, currentY, width, shapedRun.GlyphRun.Bounds.Height);
+ return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height);
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 13bc4ed124..2361ea9011 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -70,6 +70,15 @@ namespace Avalonia.Controls
Brushes.Black,
inherits: true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LineHeightProperty =
+ AvaloniaProperty.Register(
+ nameof(LineHeight),
+ double.NaN,
+ validate: IsValidLineHeight);
+
///
/// Defines the property.
///
@@ -122,19 +131,19 @@ namespace Avalonia.Controls
{
ClipToBoundsProperty.OverrideDefaultValue(true);
- AffectsRender(BackgroundProperty, ForegroundProperty,
+ AffectsRender(BackgroundProperty, ForegroundProperty,
TextAlignmentProperty, TextDecorationsProperty);
- AffectsMeasure(FontSizeProperty, FontWeightProperty,
- FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
- TextTrimmingProperty, TextProperty, PaddingProperty);
+ AffectsMeasure(FontSizeProperty, FontWeightProperty,
+ FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
+ TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed,
FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
- PaddingProperty.Changed
+ PaddingProperty.Changed, MaxLinesProperty.Changed, LineHeightProperty.Changed
).AddClassHandler((x, _) => x.InvalidateTextLayout());
}
@@ -230,6 +239,15 @@ namespace Avalonia.Controls
set { SetValue(ForegroundProperty, value); }
}
+ ///
+ /// Gets or sets the height of each line of content.
+ ///
+ public double LineHeight
+ {
+ get => GetValue(LineHeightProperty);
+ set => SetValue(LineHeightProperty, value);
+ }
+
///
/// Gets or sets the maximum number of text lines.
///
@@ -395,7 +413,7 @@ namespace Avalonia.Controls
var padding = Padding;
- TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
+ TextLayout?.Draw(context, new Point(padding.Left, padding.Top));
}
///
@@ -422,7 +440,8 @@ namespace Avalonia.Controls
TextDecorations,
constraint.Width,
constraint.Height,
- MaxLines);
+ maxLines: MaxLines,
+ lineHeight: LineHeight);
}
///
@@ -471,5 +490,7 @@ namespace Avalonia.Controls
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
+
+ private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
}
}
diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs
index f9410afe6a..bc979c15ee 100644
--- a/src/Avalonia.Visuals/Media/FontManager.cs
+++ b/src/Avalonia.Visuals/Media/FontManager.cs
@@ -100,7 +100,7 @@ namespace Avalonia.Media
return typeface;
}
- typeface = new Typeface(fontFamily, fontWeight, fontStyle);
+ typeface = new Typeface(fontFamily, fontStyle, fontWeight);
if (_typefaceCache.TryAdd(key, typeface))
{
@@ -143,7 +143,7 @@ namespace Avalonia.Media
}
var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
- _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
+ _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) :
null;
return matchedTypeface;
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
index 29c9d93560..a32a3e1b6c 100644
--- a/src/Avalonia.Visuals/Media/GlyphRun.cs
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media
{
@@ -205,13 +205,16 @@ namespace Avalonia.Media
var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex);
- var currentCluster = _glyphClusters[glyphIndex];
-
- if (characterHit.TrailingLength > 0)
+ if (!GlyphClusters.IsEmpty)
{
- while (glyphIndex < _glyphClusters.Length && _glyphClusters[glyphIndex] == currentCluster)
+ var currentCluster = GlyphClusters[glyphIndex];
+
+ if (characterHit.TrailingLength > 0)
{
- glyphIndex++;
+ while (glyphIndex < GlyphClusters.Length && GlyphClusters[glyphIndex] == currentCluster)
+ {
+ glyphIndex++;
+ }
}
}
@@ -302,7 +305,7 @@ namespace Avalonia.Media
}
}
- var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
+ var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
@@ -370,26 +373,31 @@ namespace Avalonia.Media
///
public int FindGlyphIndex(int characterIndex)
{
+ if (GlyphClusters.IsEmpty)
+ {
+ return characterIndex;
+ }
+
if (IsLeftToRight)
{
- if (characterIndex < _glyphClusters[0])
+ if (characterIndex < GlyphClusters[0])
{
return 0;
}
- if (characterIndex > _glyphClusters[_glyphClusters.Length - 1])
+ if (characterIndex > GlyphClusters[GlyphClusters.Length - 1])
{
return _glyphClusters.End;
}
}
else
{
- if (characterIndex < _glyphClusters[_glyphClusters.Length - 1])
+ if (characterIndex < GlyphClusters[GlyphClusters.Length - 1])
{
return _glyphClusters.End;
}
- if (characterIndex > _glyphClusters[0])
+ if (characterIndex > GlyphClusters[0])
{
return 0;
}
@@ -397,7 +405,7 @@ namespace Avalonia.Media
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
- var clusters = _glyphClusters.Buffer.Span;
+ var clusters = GlyphClusters.Buffer.Span;
// Find the start of the cluster at the character index.
var start = clusters.BinarySearch((ushort)characterIndex, comparer);
@@ -418,9 +426,19 @@ namespace Avalonia.Media
}
}
- while (start > 0 && clusters[start - 1] == clusters[start])
+ if (IsLeftToRight)
{
- start--;
+ while (start > 0 && clusters[start - 1] == clusters[start])
+ {
+ start--;
+ }
+ }
+ else
+ {
+ while (start + 1 < clusters.Length && clusters[start + 1] == clusters[start])
+ {
+ start++;
+ }
}
return start;
@@ -440,34 +458,74 @@ namespace Avalonia.Media
var start = FindGlyphIndex(index);
- var currentCluster = _glyphClusters[start];
+ if (GlyphClusters.IsEmpty)
+ {
+ width = GetGlyphWidth(index);
+
+ return new CharacterHit(start, 1);
+ }
- var trailingLength = 0;
+ var cluster = GlyphClusters[start];
- while (start < _glyphClusters.Length && _glyphClusters[start] == currentCluster)
+ var nextCluster = cluster;
+
+ var currentIndex = start;
+
+ while (nextCluster == cluster)
{
- if (GlyphAdvances.IsEmpty)
+ width += GetGlyphWidth(currentIndex);
+
+ if (IsLeftToRight)
{
- var glyph = GlyphIndices[start];
+ currentIndex++;
- width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ if (currentIndex == GlyphClusters.Length)
+ {
+ break;
+ }
}
else
{
- width += GlyphAdvances[start];
+ currentIndex--;
+
+ if (currentIndex < 0)
+ {
+ break;
+ }
}
- trailingLength++;
- start++;
+ nextCluster = GlyphClusters[currentIndex];
+ }
+
+ int trailingLength;
+
+ if (nextCluster == cluster)
+ {
+ trailingLength = Characters.Start + Characters.Length - cluster;
+ }
+ else
+ {
+ trailingLength = nextCluster - cluster;
}
- if (start == _glyphClusters.Length &&
- currentCluster + trailingLength != Characters.Start + Characters.Length)
+ return new CharacterHit(cluster, trailingLength);
+ }
+
+ ///
+ /// Gets a glyph's width.
+ ///
+ /// The glyph index.
+ /// The glyph's width.
+ private double GetGlyphWidth(int index)
+ {
+ if (GlyphAdvances.IsEmpty)
{
- trailingLength = Characters.Start + Characters.Length - currentCluster;
+ var glyph = GlyphIndices[index];
+
+ return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
- return new CharacterHit(currentCluster, trailingLength);
+ return GlyphAdvances[index];
}
///
diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs
index a83555946b..681fc5d499 100644
--- a/src/Avalonia.Visuals/Media/TextDecoration.cs
+++ b/src/Avalonia.Visuals/Media/TextDecoration.cs
@@ -1,4 +1,5 @@
-using Avalonia.Media.Immutable;
+using Avalonia.Collections;
+using Avalonia.Media.TextFormatting;
namespace Avalonia.Media
{
@@ -14,28 +15,52 @@ namespace Avalonia.Media
AvaloniaProperty.Register(nameof(Location));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty PenProperty =
- AvaloniaProperty.Register(nameof(Pen));
+ public static readonly StyledProperty StrokeProperty =
+ AvaloniaProperty.Register(nameof(Stroke));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty PenThicknessUnitProperty =
- AvaloniaProperty.Register(nameof(PenThicknessUnit));
+ public static readonly StyledProperty StrokeThicknessUnitProperty =
+ AvaloniaProperty.Register(nameof(StrokeThicknessUnit));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty PenOffsetProperty =
- AvaloniaProperty.Register(nameof(PenOffset));
+ public static readonly StyledProperty> StrokeDashArrayProperty =
+ AvaloniaProperty.Register>(nameof(StrokeDashArray));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty PenOffsetUnitProperty =
- AvaloniaProperty.Register(nameof(PenOffsetUnit));
+ public static readonly StyledProperty StrokeDashOffsetProperty =
+ AvaloniaProperty.Register(nameof(StrokeDashOffset));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty StrokeThicknessProperty =
+ AvaloniaProperty.Register(nameof(StrokeThickness), 1);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty StrokeLineCapProperty =
+ AvaloniaProperty.Register(nameof(StrokeLineCap));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty StrokeOffsetProperty =
+ AvaloniaProperty.Register(nameof(StrokeOffset));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty StrokeOffsetUnitProperty =
+ AvaloniaProperty.Register(nameof(StrokeOffsetUnit));
///
/// Gets or sets the location.
@@ -50,54 +75,139 @@ namespace Avalonia.Media
}
///
- /// Gets or sets the pen.
+ /// Gets or sets the that specifies how the is painted.
///
- ///
- /// The pen.
- ///
- public IPen Pen
+ public IBrush Stroke
+ {
+ get { return GetValue(StrokeProperty); }
+ set { SetValue(StrokeProperty, value); }
+ }
+
+ ///
+ /// Gets the units in which the thickness of the is expressed.
+ ///
+ public TextDecorationUnit StrokeThicknessUnit
+ {
+ get => GetValue(StrokeThicknessUnitProperty);
+ set => SetValue(StrokeThicknessUnitProperty, value);
+ }
+
+ ///
+ /// Gets or sets a collection of values that indicate the pattern of dashes and gaps
+ /// that is used to draw the .
+ ///
+ public AvaloniaList StrokeDashArray
+ {
+ get { return GetValue(StrokeDashArrayProperty); }
+ set { SetValue(StrokeDashArrayProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
+ ///
+ public double StrokeDashOffset
+ {
+ get { return GetValue(StrokeDashOffsetProperty); }
+ set { SetValue(StrokeDashOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the thickness of the .
+ ///
+ public double StrokeThickness
{
- get => GetValue(PenProperty);
- set => SetValue(PenProperty, value);
+ get { return GetValue(StrokeThicknessProperty); }
+ set { SetValue(StrokeThicknessProperty, value); }
}
///
- /// Gets the units in which the Thickness of the text decoration's is expressed.
+ /// Gets or sets a enumeration value that describes the shape at the ends of a line.
///
- public TextDecorationUnit PenThicknessUnit
+ public PenLineCap StrokeLineCap
{
- get => GetValue(PenThicknessUnitProperty);
- set => SetValue(PenThicknessUnitProperty, value);
+ get { return GetValue(StrokeLineCapProperty); }
+ set { SetValue(StrokeLineCapProperty, value); }
}
///
- /// Gets or sets the pen offset.
+ /// The stroke's offset.
///
///
/// The pen offset.
///
- public double PenOffset
+ public double StrokeOffset
{
- get => GetValue(PenOffsetProperty);
- set => SetValue(PenOffsetProperty, value);
+ get => GetValue(StrokeOffsetProperty);
+ set => SetValue(StrokeOffsetProperty, value);
}
///
- /// Gets the units in which the value is expressed.
+ /// Gets the units in which the value is expressed.
///
- public TextDecorationUnit PenOffsetUnit
+ public TextDecorationUnit StrokeOffsetUnit
{
- get => GetValue(PenOffsetUnitProperty);
- set => SetValue(PenOffsetUnitProperty, value);
+ get => GetValue(StrokeOffsetUnitProperty);
+ set => SetValue(StrokeOffsetUnitProperty, value);
}
///
- /// Creates an immutable clone of the .
+ /// Draws the at given origin.
///
- /// The immutable clone.
- public ImmutableTextDecoration ToImmutable()
+ /// The drawing context.
+ /// The shaped characters that are decorated.
+ /// The origin.
+ internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin)
{
- return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit);
+ var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize;
+ var fontMetrics = shapedTextCharacters.FontMetrics;
+ var thickness = StrokeThickness;
+
+ switch (StrokeThicknessUnit)
+ {
+ case TextDecorationUnit.FontRecommended:
+ switch (Location)
+ {
+ case TextDecorationLocation.Underline:
+ thickness = fontMetrics.UnderlineThickness;
+ break;
+ case TextDecorationLocation.Strikethrough:
+ thickness = fontMetrics.StrikethroughThickness;
+ break;
+ }
+
+ break;
+ case TextDecorationUnit.FontRenderingEmSize:
+ thickness = fontRenderingEmSize * thickness;
+ break;
+ }
+
+ switch (Location)
+ {
+ case TextDecorationLocation.Overline:
+ origin += new Point(0, fontMetrics.Ascent);
+ break;
+ case TextDecorationLocation.Strikethrough:
+ origin += new Point(0, -fontMetrics.StrikethroughPosition);
+ break;
+ case TextDecorationLocation.Underline:
+ origin += new Point(0, -fontMetrics.UnderlinePosition);
+ break;
+ }
+
+ switch (StrokeOffsetUnit)
+ {
+ case TextDecorationUnit.FontRenderingEmSize:
+ origin += new Point(0, StrokeOffset * fontRenderingEmSize);
+ break;
+ case TextDecorationUnit.Pixel:
+ origin += new Point(0, StrokeOffset);
+ break;
+ }
+
+ var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness,
+ new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
+
+ drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0));
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
index 21e2e2484c..2dced2252e 100644
--- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
+++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
-using Avalonia.Media.Immutable;
using Avalonia.Utilities;
namespace Avalonia.Media
@@ -11,22 +10,6 @@ namespace Avalonia.Media
///
public class TextDecorationCollection : AvaloniaList
{
- ///
- /// Creates an immutable clone of the .
- ///
- /// The immutable clone.
- public ImmutableTextDecoration[] ToImmutable()
- {
- var immutable = new ImmutableTextDecoration[Count];
-
- for (var i = 0; i < Count; i++)
- {
- immutable[i] = this[i].ToImmutable();
- }
-
- return immutable;
- }
-
///
/// Parses a string.
///
diff --git a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs
index dde425bb94..a61983e8d5 100644
--- a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs
+++ b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs
@@ -1,7 +1,7 @@
namespace Avalonia.Media
{
///
- /// Specifies the unit type of either a or a thickness value.
+ /// Specifies the unit type of either a or a value.
///
public enum TextDecorationUnit
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 4903342cea..56790cc0db 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -1,6 +1,4 @@
-using Avalonia.Platform;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// A text run that supports drawing content.
@@ -17,6 +15,6 @@ namespace Avalonia.Media.TextFormatting
///
/// The drawing context.
/// The origin.
- public abstract void Draw(IDrawingContextImpl drawingContext, Point origin);
+ public abstract void Draw(DrawingContext drawingContext, Point origin);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
new file mode 100644
index 0000000000..c4302aecec
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -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;
+
+ ///
+ /// Set text alignment
+ ///
+ internal void SetTextAlignment(TextAlignment textAlignment)
+ {
+ _textAlignment = textAlignment;
+ }
+
+ ///
+ /// Set text wrap
+ ///
+ internal void SetTextWrapping(TextWrapping textWrapping)
+ {
+ _textWrapping = textWrapping;
+ }
+ ///
+ /// Set text trimming
+ ///
+ internal void SetTextTrimming(TextTrimming textTrimming)
+ {
+ _textTrimming = textTrimming;
+ }
+
+ ///
+ /// Set line height
+ ///
+ internal void SetLineHeight(double lineHeight)
+ {
+ _lineHeight = lineHeight;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
new file mode 100644
index 0000000000..3db3589498
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
@@ -0,0 +1,40 @@
+using System.Globalization;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Generic implementation of TextRunProperties
+ ///
+ 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;
+ }
+
+ ///
+ public override Typeface Typeface { get; }
+
+ ///
+ public override double FontRenderingEmSize { get; }
+
+ ///
+ public override TextDecorationCollection TextDecorations { get; }
+
+ ///
+ public override IBrush ForegroundBrush { get; }
+
+ ///
+ public override IBrush BackgroundBrush { get; }
+
+ ///
+ public override CultureInfo CultureInfo { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs
new file mode 100644
index 0000000000..0c6c722941
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs
@@ -0,0 +1,23 @@
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// A group of characters that can be shaped.
+ ///
+ public sealed class ShapeableTextCharacters : TextRun
+ {
+ public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties)
+ {
+ TextSourceLength = text.Length;
+ Text = text;
+ Properties = properties;
+ }
+
+ public override int TextSourceLength { get; }
+
+ public override ReadOnlySlice Text { get; }
+
+ public override TextRunProperties Properties { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
new file mode 100644
index 0000000000..2e7e7aceb1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -0,0 +1,164 @@
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// A text run that holds shaped characters.
+ ///
+ 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;
+ }
+
+ ///
+ public override ReadOnlySlice Text { get; }
+
+ ///
+ public override TextRunProperties Properties { get; }
+
+ ///
+ public override int TextSourceLength { get; }
+
+ ///
+ public override Rect Bounds => GlyphRun.Bounds;
+
+ ///
+ /// Gets the font metrics.
+ ///
+ ///
+ /// The font metrics.
+ ///
+ public FontMetrics FontMetrics { get; }
+
+ ///
+ /// Gets the glyph run.
+ ///
+ ///
+ /// The glyphs.
+ ///
+ public GlyphRun GlyphRun { get; }
+
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Splits the at specified length.
+ ///
+ /// The length.
+ /// The split result.
+ 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;
+ }
+
+ ///
+ /// Gets the first text run.
+ ///
+ ///
+ /// The first text run.
+ ///
+ public ShapedTextCharacters First { get; }
+
+ ///
+ /// Gets the second text run.
+ ///
+ ///
+ /// The second text run.
+ ///
+ public ShapedTextCharacters Second { get; }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
deleted file mode 100644
index 00f9b918cb..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using Avalonia.Media.Immutable;
-using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Platform;
-using Avalonia.Utility;
-
-namespace Avalonia.Media.TextFormatting
-{
- ///
- /// A text run that holds a shaped glyph run.
- ///
- public sealed class ShapedTextRun : DrawableTextRun
- {
- public ShapedTextRun(ReadOnlySlice text, TextStyle style) : this(
- TextShaper.Current.ShapeText(text, style.TextFormat), style)
- {
- }
-
- public ShapedTextRun(GlyphRun glyphRun, TextStyle style)
- {
- Text = glyphRun.Characters;
- Style = style;
- GlyphRun = glyphRun;
- }
-
- ///
- public override Rect Bounds => GlyphRun.Bounds;
-
- ///
- /// Gets the glyph run.
- ///
- ///
- /// The glyphs.
- ///
- public GlyphRun GlyphRun { get; }
-
- ///
- 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);
- }
- }
-
- ///
- /// Draws the at given origin.
- ///
- /// The drawing context.
- /// The text decoration.
- /// The origin.
- 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));
- }
-
- ///
- /// Splits the at specified length.
- ///
- /// The length.
- /// The split result.
- 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;
- }
-
- ///
- /// Gets the first text run.
- ///
- ///
- /// The first text run.
- ///
- public ShapedTextRun First { get; }
-
- ///
- /// Gets the second text run.
- ///
- ///
- /// The second text run.
- ///
- public ShapedTextRun Second { get; }
- }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
deleted file mode 100644
index f84e45d4c6..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
+++ /dev/null
@@ -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 s_ellipsis = new ReadOnlySlice(new[] { '\u2026' });
-
- ///
- 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;
- }
-
- ///
- /// Formats text runs with optional text style overrides.
- ///
- /// The text source.
- /// The first text source index.
- /// The text pointer that covers the formatted text runs.
- ///
- /// The formatted text runs.
- ///
- private List FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
- {
- var start = -1;
- var length = 0;
-
- var textRuns = new List();
-
- 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;
- }
-
- ///
- /// Performs text trimming and returns a trimmed line.
- ///
- /// A value that specifies the width of the paragraph that the line fills.
- /// A value that represents paragraph properties,
- /// such as TextWrapping, TextAlignment, or TextStyle.
- /// The text runs to perform the trimming on.
- /// The text that was used to construct the text runs.
- ///
- private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList 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(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));
- }
-
- ///
- /// Performs text wrapping returns a list of text lines.
- ///
- /// The text paragraph properties.
- /// The text run'S.
- /// The text to analyze for break opportunities.
- ///
- ///
- private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList 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));
- }
-
- ///
- /// Measures the number of characters that fits into available width.
- ///
- /// The text run.
- /// The available width.
- ///
- 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;
- }
-
- ///
- /// Creates an ellipsis.
- ///
- /// The text style.
- ///
- private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle)
- {
- var formatterImpl = AvaloniaLocator.Current.GetService();
-
- var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat);
-
- return new ShapedTextRun(glyphRun, textStyle);
- }
-
- private readonly struct SplitTextRunsResult
- {
- public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second)
- {
- First = first;
-
- Second = second;
- }
-
- ///
- /// Gets the first text runs.
- ///
- ///
- /// The first text runs.
- ///
- public IReadOnlyList First { get; }
-
- ///
- /// Gets the second text runs.
- ///
- ///
- /// The second text runs.
- ///
- public IReadOnlyList Second { get; }
- }
-
- ///
- /// Split a sequence of runs into two segments at specified length.
- ///
- /// The text run's.
- /// The length to split at.
- ///
- private static SplitTextRunsResult SplitTextRuns(IReadOnlyList 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);
- }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
deleted file mode 100644
index 11d241bc34..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
+++ /dev/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 _textRuns;
-
- public SimpleTextLine(TextPointer textPointer, IReadOnlyList textRuns, TextLineMetrics lineMetrics)
- {
- Text = textPointer;
- _textRuns = textRuns;
- LineMetrics = lineMetrics;
- }
-
- ///
- public override TextPointer Text { get; }
-
- ///
- public override IReadOnlyList TextRuns => _textRuns;
-
- ///
- public override TextLineMetrics LineMetrics { get; }
-
- ///
- 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;
- }
- }
-
- ///
- 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;
- }
-
- ///
- public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
- {
- return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
- }
-
- ///
- 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;
- }
-
- ///
- 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;
- }
-
- ///
- public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
- {
- // same operation as move-to-previous
- return GetPreviousCaretCharacterHit(characterHit);
- }
-
- ///
- /// Get distance from line start to the specified codepoint index
- ///
- 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;
- }
-
- ///
- /// 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.
- ///
- 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;
- }
-
- ///
- /// 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.
- ///
- 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;
- }
- }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index d9b27958ab..b35882fc0e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -1,4 +1,6 @@
-using Avalonia.Utility;
+using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -7,15 +9,182 @@ namespace Avalonia.Media.TextFormatting
///
public class TextCharacters : TextRun
{
- protected TextCharacters()
+ public TextCharacters(ReadOnlySlice text, TextRunProperties properties)
{
-
+ TextSourceLength = text.Length;
+ Text = text;
+ Properties = properties;
}
- public TextCharacters(ReadOnlySlice text, TextStyle style)
+ ///
+ public override int TextSourceLength { get; }
+
+ ///
+ public override ReadOnlySlice Text { get; }
+
+ ///
+ public override TextRunProperties Properties { get; }
+
+ ///
+ /// Gets a list of .
+ ///
+ /// The shapeable text characters.
+ internal IList GetShapeableCharacters()
{
- Text = text;
- Style = style;
+ var shapeableCharacters = new List(2);
+
+ var runText = Text;
+
+ while (!runText.IsEmpty)
+ {
+ var shapeableRun = CreateShapeableRun(runText, Properties);
+
+ shapeableCharacters.Add(shapeableRun);
+
+ runText = runText.Skip(shapeableRun.Text.Length);
+ }
+
+ return shapeableCharacters;
+ }
+
+ ///
+ /// Creates a shapeable text run with unique properties.
+ ///
+ /// The text to create text runs from.
+ /// The default text run properties.
+ /// A list of shapeable text runs.
+ private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties)
+ {
+ var defaultTypeface = defaultProperties.Typeface;
+
+ var currentTypeface = defaultTypeface;
+
+ if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
+ {
+ return new ShapeableTextCharacters(text.Take(count),
+ new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
+
+ }
+
+ var codepoint = Codepoint.ReadAt(text, count, out _);
+
+ //ToDo: Fix FontFamily fallback
+ currentTypeface =
+ FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultTypeface.FontFamily);
+
+ if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
+ {
+ //Fallback found
+ return new ShapeableTextCharacters(text.Take(count),
+ new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
+ }
+
+ // no fallback found
+ currentTypeface = defaultTypeface;
+
+ var glyphTypeface = currentTypeface.GlyphTypeface;
+
+ var enumerator = new GraphemeEnumerator(text);
+
+ while (enumerator.MoveNext())
+ {
+ var grapheme = enumerator.Current;
+
+ if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+
+ count += grapheme.Text.Length;
+ }
+
+ return new ShapeableTextCharacters(text.Take(count),
+ new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
+ defaultProperties.TextDecorations, defaultProperties.ForegroundBrush));
+ }
+
+ ///
+ /// Tries to get run properties.
+ ///
+ ///
+ ///
+ /// The typeface that is used to find matching characters.
+ ///
+ ///
+ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface,
+ out int count)
+ {
+ if (text.Length == 0)
+ {
+ count = 0;
+ return false;
+ }
+
+ var isFallback = typeface != defaultTypeface;
+
+ count = 0;
+ var script = Script.Common;
+ //var direction = BiDiClass.LeftToRight;
+
+ var font = typeface.GlyphTypeface;
+ var defaultFont = defaultTypeface.GlyphTypeface;
+
+ var enumerator = new GraphemeEnumerator(text);
+
+ while (enumerator.MoveNext())
+ {
+ var grapheme = enumerator.Current;
+
+ var currentScript = grapheme.FirstCodepoint.Script;
+
+ //var currentDirection = grapheme.FirstCodepoint.BiDiClass;
+
+ //// ToDo: Implement BiDi algorithm
+ //if (currentScript.HorizontalDirection != direction)
+ //{
+ // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
+ // {
+ // break;
+ // }
+ //}
+
+ if (currentScript != script)
+ {
+ if (currentScript != Script.Inherited && currentScript != Script.Common)
+ {
+ if (script == Script.Inherited || script == Script.Common)
+ {
+ script = currentScript;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ if (isFallback)
+ {
+ if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+ }
+
+ if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _))
+ {
+ if (!grapheme.FirstCodepoint.IsWhiteSpace)
+ {
+ break;
+ }
+ }
+
+ count += grapheme.Text.Length;
+ }
+
+ return count > 0;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs
deleted file mode 100644
index 18dd6c7c10..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-
-namespace Avalonia.Media.TextFormatting
-{
- ///
- /// Unique text formatting properties that are used by the .
- ///
- public readonly struct TextFormat : IEquatable
- {
- public TextFormat(Typeface typeface, double fontRenderingEmSize)
- {
- Typeface = typeface;
- FontRenderingEmSize = fontRenderingEmSize;
- FontMetrics = new FontMetrics(typeface, fontRenderingEmSize);
- }
-
- ///
- /// Gets the typeface.
- ///
- ///
- /// The typeface.
- ///
- public Typeface Typeface { get; }
-
- ///
- /// Gets the font rendering em size.
- ///
- ///
- /// The em rendering size of the font.
- ///
- public double FontRenderingEmSize { get; }
-
- ///
- /// Gets the font metrics.
- ///
- ///
- /// The metrics of the font.
- ///
- 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;
- }
- }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
index 7da39dc5dc..e4c898e2b8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
@@ -1,5 +1,4 @@
using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utility;
namespace Avalonia.Media.TextFormatting
{
@@ -22,7 +21,7 @@ namespace Avalonia.Media.TextFormatting
return current;
}
- current = new SimpleTextFormatter();
+ current = new TextFormatterImpl();
AvaloniaLocator.CurrentMutable.Bind().ToConstant(current);
@@ -38,149 +37,10 @@ namespace Avalonia.Media.TextFormatting
/// A value that specifies the width of the paragraph that the line fills.
/// A value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.
+ /// A value that specifies the text formatter state,
+ /// in terms of where the previous line in the paragraph was broken by the text formatting process.
/// The formatted line.
public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
- TextParagraphProperties paragraphProperties);
-
- ///
- /// Creates a text style run with unique properties.
- ///
- /// The text to create text runs from.
- ///
- /// A list of text runs.
- protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice text, TextStyle defaultStyle)
- {
- var defaultTypeface = defaultStyle.TextFormat.Typeface;
-
- var currentTypeface = defaultTypeface;
-
- if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
- {
- return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
- defaultStyle.TextFormat.FontRenderingEmSize,
- defaultStyle.Foreground, defaultStyle.TextDecorations));
-
- }
-
- var codepoint = Codepoint.ReadAt(text, count, out _);
-
- //ToDo: Fix FontFamily fallback
- currentTypeface =
- FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
-
- if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
- {
- //Fallback found
- return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
- defaultStyle.TextFormat.FontRenderingEmSize,
- defaultStyle.Foreground, defaultStyle.TextDecorations));
-
- }
-
- // no fallback found
- currentTypeface = defaultTypeface;
-
- var glyphTypeface = currentTypeface.GlyphTypeface;
-
- var enumerator = new GraphemeEnumerator(text);
-
- while (enumerator.MoveNext())
- {
- var grapheme = enumerator.Current;
-
- if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
- {
- break;
- }
-
- count += grapheme.Text.Length;
- }
-
- return new TextStyleRun(new TextPointer(text.Start, count),
- new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize,
- defaultStyle.Foreground, defaultStyle.TextDecorations));
- }
-
- ///
- /// Tries to get run properties.
- ///
- ///
- ///
- /// The typeface that is used to find matching characters.
- ///
- ///
- protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface,
- out int count)
- {
- if (text.Length == 0)
- {
- count = 0;
- return false;
- }
-
- var isFallback = typeface != defaultTypeface;
-
- count = 0;
- var script = Script.Common;
- //var direction = BiDiClass.LeftToRight;
-
- var font = typeface.GlyphTypeface;
- var defaultFont = defaultTypeface.GlyphTypeface;
-
- var enumerator = new GraphemeEnumerator(text);
-
- while (enumerator.MoveNext())
- {
- var grapheme = enumerator.Current;
-
- var currentScript = grapheme.FirstCodepoint.Script;
-
- //var currentDirection = grapheme.FirstCodepoint.BiDiClass;
-
- //// ToDo: Implement BiDi algorithm
- //if (currentScript.HorizontalDirection != direction)
- //{
- // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
- // {
- // break;
- // }
- //}
-
- if (currentScript != script)
- {
- if (currentScript != Script.Inherited && currentScript != Script.Common)
- {
- if (script == Script.Inherited || script == Script.Common)
- {
- script = currentScript;
- }
- else
- {
- break;
- }
- }
- }
-
- if (isFallback)
- {
- if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _))
- {
- break;
- }
- }
-
- if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _))
- {
- if (!grapheme.FirstCodepoint.IsWhiteSpace)
- {
- break;
- }
- }
-
- count += grapheme.Text.Length;
- }
-
- return count > 0;
- }
+ TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
new file mode 100644
index 0000000000..793707d0b2
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -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 s_ellipsis = new ReadOnlySlice(new[] { '\u2026' });
+
+ ///
+ 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;
+ }
+
+ ///
+ /// Fetches text runs.
+ ///
+ /// The text source.
+ /// The first text source index.
+ /// Previous line break. Can be null.
+ /// Next line break. Can be null.
+ ///
+ /// The formatted text runs.
+ ///
+ private static IReadOnlyList FetchTextRuns(ITextSource textSource,
+ int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak)
+ {
+ nextLineBreak = default;
+
+ var currentLength = 0;
+
+ var textRuns = new List();
+
+ 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;
+ }
+
+ ///
+ /// Performs text trimming and returns a trimmed line.
+ ///
+ /// The text runs to perform the trimming on.
+ /// The text range that is covered by the text runs.
+ /// A value that specifies the width of the paragraph that the line fills.
+ /// A value that represents paragraph properties,
+ /// such as TextWrapping, TextAlignment, or TextStyle.
+ ///
+ private static TextLine PerformTextTrimming(IReadOnlyList 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(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));
+ }
+
+ ///
+ /// Performs text wrapping returns a list of text lines.
+ ///
+ /// The text run's.
+ /// The text range that is covered by the text runs.
+ /// The paragraph width.
+ /// The text paragraph properties.
+ /// The wrapped text line.
+ private static TextLine PerformTextWrapping(IReadOnlyList 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));
+ }
+
+ ///
+ /// Measures the number of characters that fits into available width.
+ ///
+ /// The text run.
+ /// The available width.
+ ///
+ 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;
+ }
+
+ ///
+ /// Creates an ellipsis.
+ ///
+ /// The text run properties.
+ ///
+ private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
+ {
+ var formatterImpl = AvaloniaLocator.Current.GetService();
+
+ var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
+ properties.CultureInfo);
+
+ return new ShapedTextCharacters(glyphRun, properties);
+ }
+
+ ///
+ /// Gets the text range that is covered by the text runs.
+ ///
+ /// The text runs.
+ /// The text range that is covered by the text runs.
+ private static TextRange GetTextRange(IReadOnlyList 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);
+ }
+
+ ///
+ /// Split a sequence of runs into two segments at specified length.
+ ///
+ /// The text run's.
+ /// The length to split at.
+ /// The split text runs.
+ private static SplitTextRunsResult SplitTextRuns(IReadOnlyList 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 first, IReadOnlyList second)
+ {
+ First = first;
+
+ Second = second;
+ }
+
+ ///
+ /// Gets the first text runs.
+ ///
+ ///
+ /// The first text runs.
+ ///
+ public IReadOnlyList First { get; }
+
+ ///
+ /// Gets the second text runs.
+ ///
+ ///
+ /// The second text runs.
+ ///
+ public IReadOnlyList 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);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 0292398782..2e2e4a8c68 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Platform;
using Avalonia.Utilities;
-using Avalonia.Utility;
+using Avalonia.Platform;
namespace Avalonia.Media.TextFormatting
{
@@ -14,11 +12,11 @@ namespace Avalonia.Media.TextFormatting
///
public class TextLayout
{
- private static readonly ReadOnlySlice s_empty = new ReadOnlySlice(new[] { '\u200B' });
+ private static readonly char[] s_empty = { '\u200B' };
private readonly ReadOnlySlice _text;
private readonly TextParagraphProperties _paragraphProperties;
- private readonly IReadOnlyList _textStyleOverrides;
+ private readonly IReadOnlyList> _textStyleOverrides;
///
/// Initializes a new instance of the class.
@@ -33,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// The text decorations.
/// The maximum width.
/// The maximum height.
+ /// The height of each line of text.
/// The maximum number of text lines.
/// The text style overrides.
public TextLayout(
@@ -46,18 +45,22 @@ namespace Avalonia.Media.TextFormatting
TextDecorationCollection textDecorations = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
+ double lineHeight = double.NaN,
int maxLines = 0,
- IReadOnlyList textStyleOverrides = null)
+ IReadOnlyList> textStyleOverrides = null)
{
_text = string.IsNullOrEmpty(text) ?
new ReadOnlySlice() :
new ReadOnlySlice(text.AsMemory());
_paragraphProperties =
- CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable());
+ CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming,
+ textDecorations, lineHeight);
_textStyleOverrides = textStyleOverrides;
+ LineHeight = lineHeight;
+
MaxWidth = maxWidth;
MaxHeight = maxHeight;
@@ -67,22 +70,29 @@ namespace Avalonia.Media.TextFormatting
UpdateLayout();
}
+ ///
+ /// Gets or sets the height of each line of text.
+ ///
+ ///
+ /// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height
+ /// is determined automatically from the current font characteristics. The default is NaN.
+ ///
+ public double LineHeight { get; }
+
///
/// Gets the maximum width.
///
public double MaxWidth { get; }
-
///
/// Gets the maximum height.
///
public double MaxHeight { get; }
-
///
/// Gets the maximum number of text lines.
///
- public double MaxLines { get; }
+ public int MaxLines { get; }
///
/// Gets the text lines.
@@ -105,7 +115,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The drawing context.
/// The origin.
- public void Draw(IDrawingContextImpl context, Point origin)
+ public void Draw(DrawingContext context, Point origin)
{
if (!TextLines.Any())
{
@@ -132,14 +142,16 @@ namespace Avalonia.Media.TextFormatting
/// The text wrapping.
/// The text trimming.
/// The text decorations.
+ /// The height of each line of text.
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming,
- ImmutableTextDecoration[] textDecorations)
+ TextDecorationCollection textDecorations, double lineHeight)
{
- var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations);
+ var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
- return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming);
+ return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming,
+ lineHeight);
}
///
@@ -170,14 +182,15 @@ namespace Avalonia.Media.TextFormatting
/// The empty text line.
private TextLine CreateEmptyTextLine(int startingIndex)
{
- var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat;
+ var properties = _paragraphProperties.DefaultTextRunProperties;
- var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat);
+ var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1),
+ properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
- var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) };
+ var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) };
- return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns,
- TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment));
+ return new TextLineImpl(textRuns,
+ TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));
}
///
@@ -199,77 +212,38 @@ namespace Avalonia.Media.TextFormatting
double left = 0.0, right = 0.0, bottom = 0.0;
- var lineBreaker = new LineBreakEnumerator(_text);
-
var currentPosition = 0;
+ var textSource = new FormattedTextSource(_text,
+ _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
+
+ TextLineBreak previousLineBreak = null;
+
while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines))
{
- int length;
+ var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
+ _paragraphProperties, previousLineBreak);
- if (lineBreaker.MoveNext())
- {
- if (!lineBreaker.Current.Required)
- {
- continue;
- }
+ previousLineBreak = textLine.LineBreak;
- length = lineBreaker.Current.PositionWrap - currentPosition;
+ textLines.Add(textLine);
- if (currentPosition + length < _text.Length)
- {
- //The line breaker isn't treating \n\r as a pair so we have to fix that here.
- if (_text[lineBreaker.Current.PositionMeasure] == '\n'
- && _text[lineBreaker.Current.PositionWrap] == '\r')
- {
- length++;
- }
- }
- }
- else
+ UpdateBounds(textLine, ref left, ref right, ref bottom);
+
+ if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight)
{
- length = _text.Length - currentPosition;
+ break;
}
- var remainingLength = length;
+ currentPosition += textLine.TextRange.Length;
- while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines))
+ if (currentPosition != _text.Length || textLine.LineBreak == null)
{
- var textSlice = _text.AsSlice(currentPosition, remainingLength);
-
- var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides);
-
- var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties);
-
- UpdateBounds(textLine, ref left, ref right, ref bottom);
-
- textLines.Add(textLine);
-
- if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
- {
- currentPosition = _text.Length;
- break;
- }
-
- if (_paragraphProperties.TextTrimming != TextTrimming.None)
- {
- currentPosition += remainingLength;
-
- break;
- }
-
- remainingLength -= textLine.Text.Length;
-
- currentPosition += textLine.Text.Length;
+ continue;
}
- }
- if (lineBreaker.Current.Required && currentPosition == _text.Length)
- {
var emptyTextLine = CreateEmptyTextLine(currentPosition);
- UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
-
textLines.Add(emptyTextLine);
}
@@ -279,22 +253,27 @@ namespace Avalonia.Media.TextFormatting
}
}
- private struct FormattedTextSource : ITextSource
+ private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
- private readonly TextStyle _defaultStyle;
- private readonly IReadOnlyList _textStyleOverrides;
+ private readonly TextRunProperties _defaultProperties;
+ private readonly IReadOnlyList> _textModifier;
- public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle,
- IReadOnlyList textStyleOverrides)
+ public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties,
+ IReadOnlyList> textModifier)
{
_text = text;
- _defaultStyle = defaultStyle;
- _textStyleOverrides = textStyleOverrides;
+ _defaultProperties = defaultProperties;
+ _textModifier = textModifier;
}
public TextRun GetTextRun(int textSourceIndex)
{
+ if (textSourceIndex > _text.End)
+ {
+ return new TextEndOfLine();
+ }
+
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
@@ -302,30 +281,29 @@ namespace Avalonia.Media.TextFormatting
return new TextEndOfLine();
}
- var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides);
+ var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
- return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style);
+ return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
///
- /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle.
- /// If optimizeForShaping is true Foreground is ignored.
+ /// Creates a span of text run properties that has modifier applied.
///
- /// The text to create the run for.
- /// The default text style for segments that don't have an override.
- /// The text style overrides.
+ /// The text to create the properties for.
+ /// The default text properties.
+ /// The text properties modifier.
///
/// The created text style run.
///
- private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text,
- TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides)
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ TextRunProperties defaultProperties, IReadOnlyList> textModifier)
{
- if(textStyleOverrides == null || textStyleOverrides.Count == 0)
+ if (textModifier == null || textModifier.Count == 0)
{
- return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle);
+ return new ValueSpan(text.Start, text.Length, defaultProperties);
}
- var currentTextStyle = defaultTextStyle;
+ var currentProperties = defaultProperties;
var hasOverride = false;
@@ -333,35 +311,34 @@ namespace Avalonia.Media.TextFormatting
var length = 0;
- for (; i < textStyleOverrides.Count; i++)
+ for (; i < textModifier.Count; i++)
{
- var styleOverride = textStyleOverrides[i];
+ var propertiesOverride = textModifier[i];
- var textPointer = styleOverride.TextPointer;
+ var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
- if (textPointer.End < text.Start)
+ if (textRange.End < text.Start)
{
continue;
}
- if (textPointer.Start > text.End)
+ if (textRange.Start > text.End)
{
length = text.Length;
break;
}
- if (textPointer.Start > text.Start)
+ if (textRange.Start > text.Start)
{
- if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat ||
- !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground))
+ if (propertiesOverride.Value != currentProperties)
{
- length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length);
+ length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
break;
}
}
- length += Math.Min(text.Length - length, textPointer.Length);
+ length += Math.Min(text.Length - length, textRange.Length);
if (hasOverride)
{
@@ -370,13 +347,12 @@ namespace Avalonia.Media.TextFormatting
hasOverride = true;
- currentTextStyle = styleOverride.Style;
+ currentProperties = propertiesOverride.Value;
}
- if (length < text.Length && i == textStyleOverrides.Count)
+ if (length < text.Length && i == textModifier.Count)
{
- if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) &&
- currentTextStyle.TextFormat == defaultTextStyle.TextFormat)
+ if (currentProperties == defaultProperties)
{
length = text.Length;
}
@@ -387,7 +363,7 @@ namespace Avalonia.Media.TextFormatting
text = text.Take(length);
}
- return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle);
+ return new ValueSpan(text.Start, length, currentProperties);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index a0f7b44882..c3b7dfc77a 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using Avalonia.Platform;
namespace Avalonia.Media.TextFormatting
{
@@ -9,12 +8,12 @@ namespace Avalonia.Media.TextFormatting
public abstract class TextLine
{
///
- /// Gets the text.
+ /// Gets the text range that is covered by the line.
///
///
- /// The text pointer.
+ /// The text range that is covered by the line.
///
- public abstract TextPointer Text { get; }
+ public abstract TextRange TextRange { get; }
///
/// Gets the text runs.
@@ -32,12 +31,20 @@ namespace Avalonia.Media.TextFormatting
///
public abstract TextLineMetrics LineMetrics { get; }
+ ///
+ /// Gets the state of the line when broken by line breaking process.
+ ///
+ ///
+ /// A value that represents the line break.
+ ///
+ public abstract TextLineBreak LineBreak { get; }
+
///
/// Draws the at the given origin.
///
/// The drawing context.
/// The origin.
- public abstract void Draw(IDrawingContextImpl drawingContext, Point origin);
+ public abstract void Draw(DrawingContext drawingContext, Point origin);
///
/// Client to get the character hit corresponding to the specified
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
new file mode 100644
index 0000000000..c24454cb76
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+ public class TextLineBreak
+ {
+ public TextLineBreak(IReadOnlyList remainingCharacters)
+ {
+ RemainingCharacters = remainingCharacters;
+ }
+
+ ///
+ /// Get the remaining shaped characters that were split up by the during the formatting process.
+ ///
+ public IReadOnlyList RemainingCharacters { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
new file mode 100644
index 0000000000..cf00399b8a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -0,0 +1,235 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+ internal class TextLineImpl : TextLine
+ {
+ private readonly IReadOnlyList _textRuns;
+
+ public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics,
+ TextLineBreak lineBreak = null)
+ {
+ _textRuns = textRuns;
+ LineMetrics = lineMetrics;
+ LineBreak = lineBreak;
+ }
+
+ ///
+ public override TextRange TextRange => LineMetrics.TextRange;
+
+ ///
+ public override IReadOnlyList TextRuns => _textRuns;
+
+ ///
+ public override TextLineMetrics LineMetrics { get; }
+
+ ///
+ public override TextLineBreak LineBreak { get; }
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
+ {
+ return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
+ }
+
+ ///
+ 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
+ }
+
+ ///
+ 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
+ }
+
+ ///
+ public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
+ {
+ // same operation as move-to-previous
+ return GetPreviousCaretCharacterHit(characterHit);
+ }
+
+ ///
+ /// Get distance from line start to the specified codepoint index.
+ ///
+ 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;
+ }
+
+ ///
+ /// Tries to find the next character hit.
+ ///
+ /// The current character hit.
+ /// The next character hit.
+ ///
+ 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;
+ }
+
+ ///
+ /// Tries to find the previous character hit.
+ ///
+ /// The current character hit.
+ /// The previous character hit.
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets the run index of the specified codepoint index.
+ ///
+ /// The codepoint index.
+ /// The text run index.
+ 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;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
index 096305c09c..d47cc0c394 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -8,38 +9,20 @@ namespace Avalonia.Media.TextFormatting
///
public readonly struct TextLineMetrics
{
- public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap)
+ public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange)
{
- Ascent = ascent;
- Descent = descent;
- LineGap = lineGap;
- Size = new Size(width, descent - ascent + lineGap);
- BaselineOrigin = new Point(xOrigin, -ascent);
+ Size = size;
+ BaselineOrigin = baselineOrigin;
+ TextRange = textRange;
}
///
- /// Gets the overall recommended distance above the baseline.
+ /// Gets the text range that is covered by the text line.
///
///
- /// The ascent.
+ /// The text range that is covered by the text line.
///
- public double Ascent { get; }
-
- ///
- /// Gets the overall recommended distance under the baseline.
- ///
- ///
- /// The descent.
- ///
- public double Descent { get; }
-
- ///
- /// Gets the overall recommended additional space between two lines of text.
- ///
- ///
- /// The leading.
- ///
- public double LineGap { get; }
+ public TextRange TextRange { get; }
///
/// Gets the size of the text line.
@@ -61,10 +44,12 @@ namespace Avalonia.Media.TextFormatting
/// Creates the text line metrics.
///
/// The text runs.
+ /// The text range that is covered by the text line.
/// The paragraph width.
- /// The text alignment.
+ /// The text alignment.
///
- public static TextLineMetrics Create(IEnumerable textRuns, double paragraphWidth, TextAlignment textAlignment)
+ public static TextLineMetrics Create(IEnumerable textRuns, TextRange textRange, double paragraphWidth,
+ TextParagraphProperties paragraphProperties)
{
var lineWidth = 0.0;
var ascent = 0.0;
@@ -73,31 +58,39 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
- var shapedRun = (ShapedTextRun)textRun;
+ var shapedRun = (ShapedTextCharacters)textRun;
- lineWidth += shapedRun.Bounds.Width;
+ var fontMetrics =
+ new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize);
- var textFormat = textRun.Style.TextFormat;
+ lineWidth += shapedRun.Bounds.Width;
- if (ascent > textRun.Style.TextFormat.FontMetrics.Ascent)
+ if (ascent > fontMetrics.Ascent)
{
- ascent = textFormat.FontMetrics.Ascent;
+ ascent = fontMetrics.Ascent;
}
- if (descent < textFormat.FontMetrics.Descent)
+ if (descent < fontMetrics.Descent)
{
- descent = textFormat.FontMetrics.Descent;
+ descent = fontMetrics.Descent;
}
- if (lineGap < textFormat.FontMetrics.LineGap)
+ if (lineGap < fontMetrics.LineGap)
{
- lineGap = textFormat.FontMetrics.LineGap;
+ lineGap = fontMetrics.LineGap;
}
}
- var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, textAlignment);
+ var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment);
+
+ var baselineOrigin = new Point(xOrigin, -ascent);
+
+ var size = new Size(lineWidth,
+ double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ?
+ descent - ascent + lineGap :
+ paragraphProperties.LineHeight);
- return new TextLineMetrics(lineWidth, xOrigin, ascent, descent, lineGap);
+ return new TextLineMetrics(size, baselineOrigin, textRange);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index 1368f1777a..39eb695404 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -3,38 +3,37 @@
///
/// Provides a set of properties that are used during the paragraph layout.
///
- public readonly struct TextParagraphProperties
+ public abstract class TextParagraphProperties
{
- public TextParagraphProperties(
- TextStyle defaultTextStyle,
- TextAlignment textAlignment = TextAlignment.Left,
- TextWrapping textWrapping = TextWrapping.NoWrap,
- TextTrimming textTrimming = TextTrimming.None)
- {
- DefaultTextStyle = defaultTextStyle;
- TextAlignment = textAlignment;
- TextWrapping = textWrapping;
- TextTrimming = textTrimming;
- }
+ ///
+ /// Gets the text alignment.
+ ///
+ public abstract TextAlignment TextAlignment { get; }
///
/// Gets the default text style.
///
- public TextStyle DefaultTextStyle { get; }
+ public abstract TextRunProperties DefaultTextRunProperties { get; }
///
- /// Gets the text alignment.
+ /// If not null, text decorations to apply to all runs in the line. This is in addition
+ /// to any text decorations specified by the TextRunProperties for individual text runs.
///
- public TextAlignment TextAlignment { get; }
+ public virtual TextDecorationCollection TextDecorations => null;
///
/// Gets the text wrapping.
///
- public TextWrapping TextWrapping { get; }
+ public abstract TextWrapping TextWrapping { get; }
///
/// Gets the text trimming.
///
- public TextTrimming TextTrimming { get; }
+ public abstract TextTrimming TextTrimming { get; }
+
+ ///
+ /// Paragraph's line height
+ ///
+ public abstract double LineHeight { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs
similarity index 73%
rename from src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs
rename to src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs
index 65d5c04b4c..1177c758f4 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs
@@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting
///
/// References a portion of a text buffer.
///
- public readonly struct TextPointer
+ public readonly struct TextRange
{
- public TextPointer(int start, int length)
+ public TextRange(int start, int length)
{
Start = start;
Length = length;
@@ -41,30 +41,30 @@ namespace Avalonia.Media.TextFormatting
/// Returns a specified number of contiguous elements from the start of the slice.
///
/// The number of elements to return.
- /// A that contains the specified number of elements from the start of this slice.
- public TextPointer Take(int length)
+ /// A that contains the specified number of elements from the start of this slice.
+ public TextRange Take(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
- return new TextPointer(Start, length);
+ return new TextRange(Start, length);
}
///
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
///
/// The number of elements to skip before returning the remaining elements.
- /// A that contains the elements that occur after the specified index in this slice.
- public TextPointer Skip(int length)
+ /// A that contains the elements that occur after the specified index in this slice.
+ public TextRange Skip(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
- return new TextPointer(Start + length, Length - length);
+ return new TextRange(Start + length, Length - length);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
index 28b83333b9..c15a771755 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
@@ -1,5 +1,5 @@
using System.Diagnostics;
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -9,15 +9,22 @@ namespace Avalonia.Media.TextFormatting
[DebuggerTypeProxy(typeof(TextRunDebuggerProxy))]
public abstract class TextRun
{
+ public static readonly int DefaultTextSourceLength = 1;
+
+ ///
+ /// Gets the text source length.
+ ///
+ public virtual int TextSourceLength => DefaultTextSourceLength;
+
///
/// Gets the text run's text.
///
- public ReadOnlySlice Text { get; protected set; }
+ public virtual ReadOnlySlice Text => default;
///
- /// Gets the text run's style.
+ /// A set of properties shared by every characters in the run
///
- public TextStyle Style { get; protected set; }
+ public virtual TextRunProperties Properties => null;
private class TextRunDebuggerProxy
{
@@ -42,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- public TextStyle Style => _textRun.Style;
+ public TextRunProperties Properties => _textRun.Properties;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
new file mode 100644
index 0000000000..bbcdfe2d8e
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Properties that can change from one run to the next, such as typeface or foreground brush.
+ ///
+ ///
+ /// 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.
+ ///
+ public abstract class TextRunProperties : IEquatable
+ {
+ ///
+ /// Run typeface
+ ///
+ public abstract Typeface Typeface { get; }
+
+ ///
+ /// Em size of font used to format and display text
+ ///
+ public abstract double FontRenderingEmSize { get; }
+
+ ///
+ /// Run TextDecorations.
+ ///
+ public abstract TextDecorationCollection TextDecorations { get; }
+
+ ///
+ /// Brush used to fill text.
+ ///
+ public abstract IBrush ForegroundBrush { get; }
+
+ ///
+ /// Brush used to paint background of run.
+ ///
+ public abstract IBrush BackgroundBrush { get; }
+
+ ///
+ /// Run text culture.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index eb3a4129bc..a02ace408f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -1,6 +1,7 @@
using System;
+using System.Globalization;
using Avalonia.Platform;
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@@ -44,9 +45,10 @@ namespace Avalonia.Media.TextFormatting
}
///
- public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat)
+ public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize,
+ CultureInfo culture)
{
- return _platformImpl.ShapeText(text, textFormat);
+ return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs
deleted file mode 100644
index cf52c3ca17..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Avalonia.Media.Immutable;
-
-namespace Avalonia.Media.TextFormatting
-{
- ///
- /// Unique text formatting properties that effect the styling of a text.
- ///
- 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;
- }
-
- ///
- /// Gets the text format.
- ///
- public TextFormat TextFormat { get; }
-
- ///
- /// Gets the foreground.
- ///
- public IBrush Foreground { get; }
-
- ///
- /// Gets the text decorations.
- ///
- public ImmutableTextDecoration[] TextDecorations { get; }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs
deleted file mode 100644
index 55f8999182..0000000000
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Avalonia.Media.TextFormatting
-{
- ///
- /// Represents a text run's style and is used during the layout process of the .
- ///
- public readonly struct TextStyleRun
- {
- public TextStyleRun(TextPointer textPointer, TextStyle style)
- {
- TextPointer = textPointer;
- Style = style;
- }
-
- ///
- /// Gets the text pointer.
- ///
- public TextPointer TextPointer { get; }
-
- ///
- /// Gets the text style.
- ///
- public TextStyle Style { get; }
- }
-}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
index 94171b7324..20fe345d93 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
@@ -1,4 +1,4 @@
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
index 2ff4952cab..9e1f748ebb 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
@@ -1,4 +1,4 @@
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs
index a6791b4a53..f268340eb9 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs
@@ -1,4 +1,4 @@
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
index fd7831dfe6..1e4ac8fe0f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
@@ -4,7 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Runtime.InteropServices;
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
index 25a32bb1a3..26f7721128 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
@@ -16,7 +16,7 @@
// Ported from: https://github.com/foliojs/linebreak
// Copied from: https://github.com/toptensoftware/RichTextKit
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs
index 56df3670bd..d649bda23f 100644
--- a/src/Avalonia.Visuals/Media/TextWrapping.cs
+++ b/src/Avalonia.Visuals/Media/TextWrapping.cs
@@ -5,6 +5,13 @@ namespace Avalonia.Media
///
public enum TextWrapping
{
+ ///
+ /// Line-breaking occurs if the line overflows the available block width.
+ /// However, a line may overflow the block width if the line breaking algorithm
+ /// cannot determine a break opportunity, as in the case of a very long word.
+ ///
+ WrapWithOverflow,
+
///
/// Text should not wrap.
///
@@ -15,4 +22,4 @@ namespace Avalonia.Media
///
Wrap
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index 7618598a3f..677e930804 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -16,11 +16,11 @@ namespace Avalonia.Media
/// Initializes a new instance of the class.
///
/// The font family.
- /// The font weight.
/// The font style.
+ /// The font weight.
public Typeface([NotNull]FontFamily fontFamily,
- FontWeight weight = FontWeight.Normal,
- FontStyle style = FontStyle.Normal)
+ FontStyle style = FontStyle.Normal,
+ FontWeight weight = FontWeight.Normal)
{
if (weight <= 0)
{
@@ -39,9 +39,9 @@ namespace Avalonia.Media
/// The font style.
/// The font weight.
public Typeface(string fontFamilyName,
- FontWeight weight = FontWeight.Normal,
- FontStyle style = FontStyle.Normal)
- : this(new FontFamily(fontFamilyName), weight, style)
+ FontStyle style = FontStyle.Normal,
+ FontWeight weight = FontWeight.Normal)
+ : this(new FontFamily(fontFamilyName), style, weight)
{
}
diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
index 4d770a6c6e..d915da2603 100644
--- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
+++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
@@ -1,6 +1,6 @@
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utility;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Utilities;
namespace Avalonia.Platform
{
@@ -13,8 +13,10 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a resulting glyph run.
///
/// The text.
- /// The text format.
+ /// The typeface.
+ /// The font rendering em size.
+ /// The culture.
/// A shaped glyph run.
- GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat);
+ GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture);
}
}
diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
similarity index 98%
rename from src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
rename to src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
index ff2b3b9363..5feaa88e26 100644
--- a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
+++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
@@ -2,9 +2,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
-using Avalonia.Utilities;
-namespace Avalonia.Utility
+namespace Avalonia.Utilities
{
///
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
@@ -47,7 +46,7 @@ namespace Avalonia.Utility
public int Length { get; }
///
- /// Gets a value that indicates whether this instance of is Empty.
+ /// Gets a value that indicates whether this instance of is Empty.
///
public bool IsEmpty => Length == 0;
diff --git a/src/Avalonia.Visuals/Utilities/ValueSpan.cs b/src/Avalonia.Visuals/Utilities/ValueSpan.cs
new file mode 100644
index 0000000000..7a10d865ef
--- /dev/null
+++ b/src/Avalonia.Visuals/Utilities/ValueSpan.cs
@@ -0,0 +1,30 @@
+namespace Avalonia.Utilities
+{
+ ///
+ /// Pairing of value and positions sharing that value.
+ ///
+ public readonly struct ValueSpan
+ {
+ public ValueSpan(int start, int length, T value)
+ {
+ Start = start;
+ Length = length;
+ Value = value;
+ }
+
+ ///
+ /// Get's the start of the span.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Get's the length of the span.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Get's the value of the span.
+ ///
+ public T Value { get; }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 5f876464e2..ade659f5eb 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -569,7 +569,7 @@ namespace Avalonia.Skia
float constraint = -1;
- if (_wrapping == TextWrapping.Wrap)
+ if (_wrapping != TextWrapping.NoWrap)
{
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
if (constraint > MAX_LINE_WIDTH)
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index 7a0823a223..786af7726c 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -1,9 +1,9 @@
using System;
+using System.Globalization;
using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
-using Avalonia.Utility;
+using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
@@ -11,7 +11,7 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat)
+ public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
{
using (var buffer = new Buffer())
{
@@ -61,9 +61,11 @@ namespace Avalonia.Skia
buffer.AddUtf16(text.Buffer.Span);
}
+ buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+
buffer.GuessSegmentProperties();
- var glyphTypeface = textFormat.Typeface.GlyphTypeface;
+ var glyphTypeface = typeface.GlyphTypeface;
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
@@ -71,7 +73,7 @@ namespace Avalonia.Skia
font.GetScale(out var scaleX, out _);
- var textScale = textFormat.FontRenderingEmSize / scaleX;
+ var textScale = fontRenderingEmSize / scaleX;
var bufferLength = buffer.Length;
@@ -101,7 +103,7 @@ namespace Avalonia.Skia
SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
}
- return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
+ return new GlyphRun(glyphTypeface, fontRenderingEmSize,
new ReadOnlySlice(glyphIndices),
new ReadOnlySlice(glyphAdvances),
new ReadOnlySlice(glyphOffsets),
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 2d2865e2b9..254b5684a4 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -1,8 +1,9 @@
-using Avalonia.Media;
+using System.Globalization;
+using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
-using Avalonia.Utility;
+using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
@@ -10,7 +11,7 @@ namespace Avalonia.Direct2D1.Media
{
internal class TextShaperImpl : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat)
+ public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
{
using (var buffer = new Buffer())
{
@@ -62,15 +63,17 @@ namespace Avalonia.Direct2D1.Media
buffer.GuessSegmentProperties();
- var glyphTypeface = textFormat.Typeface.GlyphTypeface;
+ var glyphTypeface = typeface.GlyphTypeface;
var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
+ buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+
font.Shape(buffer);
font.GetScale(out var scaleX, out _);
- var textScale = textFormat.FontRenderingEmSize / scaleX;
+ var textScale = fontRenderingEmSize / scaleX;
var len = buffer.Length;
@@ -104,7 +107,7 @@ namespace Avalonia.Direct2D1.Media
glyphOffsets[i] = new Vector(offsetX, offsetY);
}
- return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
+ return new GlyphRun(glyphTypeface, fontRenderingEmSize,
new ReadOnlySlice(glyphIndices),
new ReadOnlySlice(glyphAdvances),
new ReadOnlySlice(glyphOffsets),
diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
index 572749a58a..c6ecc0a7e5 100644
--- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
+++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
- new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold));
+ new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
var font = glyphTypeface.DWFont;
@@ -105,7 +105,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
- new Typeface(s_fontUri, FontWeight.Black, FontStyle.Italic));
+ new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
similarity index 98%
rename from tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
rename to tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
index 8d64190ebd..feed1179ef 100644
--- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
@@ -6,7 +6,7 @@ using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
-namespace Avalonia.Skia.UnitTests
+namespace Avalonia.Skia.UnitTests.Media
{
public class CustomFontManagerImpl : IFontManagerImpl
{
diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
similarity index 95%
rename from tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
rename to tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
index 8f80d89ac6..df286d709e 100644
--- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
@@ -1,13 +1,11 @@
using System;
using System.Linq;
-using System.Reflection;
using Avalonia.Media;
-using Avalonia.Platform;
using Avalonia.UnitTests;
using SkiaSharp;
using Xunit;
-namespace Avalonia.Skia.UnitTests
+namespace Avalonia.Skia.UnitTests.Media
{
public class FontManagerImplTests
{
@@ -39,7 +37,7 @@ namespace Avalonia.Skia.UnitTests
string fontName = fontManager.GetInstalledFontFamilyNames().First();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
- new Typeface(new FontFamily($"A, B, {fontName}"), FontWeight.Bold));
+ new Typeface(new FontFamily($"A, B, {fontName}"), weight: FontWeight.Bold));
var skTypeface = glyphTypeface.Typeface;
@@ -88,7 +86,7 @@ namespace Avalonia.Skia.UnitTests
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
- new Typeface(s_fontUri, FontWeight.Black, FontStyle.Italic));
+ new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var skTypeface = glyphTypeface.Typeface;
diff --git a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
similarity index 89%
rename from tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs
rename to tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
index 726052351b..f9f924e782 100644
--- a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
@@ -2,7 +2,7 @@
using Avalonia.UnitTests;
using Xunit;
-namespace Avalonia.Skia.UnitTests
+namespace Avalonia.Skia.UnitTests.Media
{
public class SKTypefaceCollectionCacheTests
{
@@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests
var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
- var typeface = new Typeface("ABC", FontWeight.Bold, FontStyle.Italic);
+ var typeface = new Typeface("ABC", FontStyle.Italic, FontWeight.Bold);
Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName);
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
new file mode 100644
index 0000000000..6a5065939e
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
@@ -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 _text;
+ private readonly TextRunProperties _defaultStyle;
+ private ReadOnlySlice> _styleSpans;
+
+ public FormattableTextSource(string text, TextRunProperties defaultStyle,
+ ReadOnlySlice> 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);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
new file mode 100644
index 0000000000..40aa862906
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
@@ -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(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
new file mode 100644
index 0000000000..045deacd0b
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
@@ -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 _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);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
new file mode 100644
index 0000000000..697cc4fec7
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -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(0, 3, defaultProperties),
+ new ValueSpan(3, 3,
+ new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)),
+ new ValueSpan(6, 3,
+ new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)),
+ new ValueSpan(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();
+
+ 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().ToConstant(new FontManager(new CustomFontManagerImpl()));
+
+ return disposable;
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
similarity index 77%
rename from tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
rename to tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index a2c9f8b8cd..5d9aa2cf97 100644
--- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -4,15 +4,33 @@ using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
+using Avalonia.Utilities;
using Xunit;
-namespace Avalonia.Skia.UnitTests
+namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
{
private static readonly string s_singleLineText = "0123456789";
private static readonly string s_multiLineText = "012345678\r\r0123456789";
+ [InlineData("01234\r01234\r", 3)]
+ [InlineData("01234\r01234", 2)]
+ [Theory]
+ public void Should_Break_Lines(string text, int numberOfLines)
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black);
+
+ Assert.Equal(numberOfLines, layout.TextLines.Count);
+ }
+ }
+
[Fact]
public void Should_Apply_TextStyleSpan_To_Text_In_Between()
{
@@ -22,17 +40,16 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(1, 2),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(1, 2,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
s_multiLineText,
- Typeface.Default,
+ Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textStyleOverrides : spans);
+ textStyleOverrides: spans);
var textLine = layout.TextLines[0];
@@ -46,7 +63,7 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("12", actual);
- Assert.Equal(foreground, textRun.Style.Foreground);
+ Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
}
@@ -61,9 +78,8 @@ namespace Avalonia.Skia.UnitTests
{
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(0, i),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(0, i,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var expected = new TextLayout(
@@ -72,22 +88,22 @@ namespace Avalonia.Skia.UnitTests
12.0f,
Brushes.Black.ToImmutable(),
textWrapping: TextWrapping.Wrap,
- maxWidth : 25);
+ maxWidth: 25);
var actual = new TextLayout(
s_multiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textWrapping : TextWrapping.Wrap,
- maxWidth : 25,
- textStyleOverrides : spans);
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 25,
+ textStyleOverrides: spans);
Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
for (var j = 0; j < actual.TextLines.Count; j++)
{
- Assert.Equal(expected.TextLines[j].Text.Length, actual.TextLines[j].Text.Length);
+ Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length);
Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
@@ -105,9 +121,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(0, 2),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(0, 2,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
@@ -115,7 +130,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textStyleOverrides : spans);
+ textStyleOverrides: spans);
var textLine = layout.TextLines[0];
@@ -130,7 +145,7 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("01", actual);
- Assert.Equal(foreground, textRun.Style.Foreground);
+ Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
}
@@ -143,9 +158,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(8, 2),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(8, 2,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)),
};
var layout = new TextLayout(
@@ -153,7 +167,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textStyleOverrides : spans);
+ textStyleOverrides: spans);
var textLine = layout.TextLines[0];
@@ -167,7 +181,7 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("89", actual);
- Assert.Equal(foreground, textRun.Style.Foreground);
+ Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
}
@@ -180,9 +194,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(0, 1),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(0, 1,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
@@ -190,7 +203,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textStyleOverrides : spans);
+ textStyleOverrides: spans);
var textLine = layout.TextLines[0];
@@ -200,7 +213,7 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal(1, textRun.Text.Length);
- Assert.Equal(foreground, textRun.Style.Foreground);
+ Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
}
@@ -215,9 +228,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(2, 2),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(2, 2,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
@@ -239,7 +251,7 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("😄", actual);
- Assert.Equal(foreground, textRun.Style.Foreground);
+ Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
}
@@ -254,7 +266,7 @@ namespace Avalonia.Skia.UnitTests
12.0f,
Brushes.Black.ToImmutable());
- Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.Text.Length));
+ Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
}
}
@@ -291,9 +303,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(0, 24),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(0, 24,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
@@ -301,8 +312,8 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- textWrapping : TextWrapping.Wrap,
- maxWidth : 180,
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 180,
textStyleOverrides: spans);
Assert.Equal(
@@ -322,9 +333,8 @@ namespace Avalonia.Skia.UnitTests
var spans = new[]
{
- new TextStyleRun(
- new TextPointer(5, 20),
- new TextStyle(Typeface.Default, 12, foreground))
+ new ValueSpan(5, 20,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var layout = new TextLayout(
@@ -332,13 +342,13 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
- maxWidth : 200,
- maxHeight : 125,
+ maxWidth: 200,
+ maxHeight: 125,
textStyleOverrides: spans);
- Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground);
- Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Style.Foreground);
- Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Style.Foreground);
+ Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Properties.ForegroundBrush);
+ Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Properties.ForegroundBrush);
+ Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Properties.ForegroundBrush);
}
}
@@ -355,7 +365,7 @@ namespace Avalonia.Skia.UnitTests
12.0f,
Brushes.Black.ToImmutable());
- var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0];
+ var shapedRun = (ShapedTextCharacters)layout.TextLines[0].TextRuns[0];
var glyphRun = shapedRun.GlyphRun;
@@ -390,7 +400,7 @@ namespace Avalonia.Skia.UnitTests
foreach (var textRun in textLine.TextRuns)
{
- var shapedRun = (ShapedTextRun)textRun;
+ var shapedRun = (ShapedTextCharacters)textRun;
var glyphRun = shapedRun.GlyphRun;
@@ -426,13 +436,13 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
- Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
+ Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
- Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
+ Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
- if(expectedLength == 7)
+ if (expectedLength == 7)
{
- Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
+ Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
}
}
}
@@ -467,7 +477,7 @@ namespace Avalonia.Skia.UnitTests
var textLine = layout.TextLines[0];
- var textRun = (ShapedTextRun)textLine.TextRuns[0];
+ var textRun = (ShapedTextCharacters)textLine.TextRuns[0];
Assert.Equal(7, textRun.Text.Length);
@@ -526,9 +536,28 @@ namespace Avalonia.Skia.UnitTests
}
}
+ [Fact]
+ public void Should_Produce_Fixed_Height_Lines()
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(
+ s_multiLineText,
+ Typeface.Default,
+ 12,
+ Brushes.Black,
+ lineHeight: 50);
+
+ foreach (var line in layout.TextLines)
+ {
+ Assert.Equal(50, line.LineMetrics.Size.Height);
+ }
+ }
+ }
+
private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
- [Fact(Skip= "Only used for profiling.")]
+ [Fact(Skip = "Only used for profiling.")]
public void Should_Wrap()
{
using (Start())
@@ -546,12 +575,12 @@ namespace Avalonia.Skia.UnitTests
}
}
- public static IDisposable Start()
+ private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
- fontManagerImpl : new CustomFontManagerImpl()));
+ fontManagerImpl: new CustomFontManagerImpl()));
return disposable;
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
new file mode 100644
index 0000000000..ed00d6aaed
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -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().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().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;
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs
deleted file mode 100644
index 8e695a11c8..0000000000
--- a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs
+++ /dev/null
@@ -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(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 _text;
- private readonly TextStyle _defaultStyle;
- private ReadOnlySlice _textStyleRuns;
-
- public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice 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 _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();
-
- 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().ToConstant(new FontManager(new CustomFontManagerImpl()));
-
- return disposable;
- }
- }
-}
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index 0772e0e9bd..fe1c34385f 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -1,19 +1,19 @@
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using System;
+using System.Globalization;
+using Avalonia.Media;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
-using Avalonia.Utility;
+using Avalonia.Utilities;
namespace Avalonia.UnitTests
{
public class MockTextShaperImpl : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat)
+ public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
{
- var glyphTypeface = textFormat.Typeface.GlyphTypeface;
+ var glyphTypeface = typeface.GlyphTypeface;
var glyphIndices = new ushort[text.Length];
- var height = textFormat.FontMetrics.LineHeight;
- var width = 0.0;
+ var glyphCount = 0;
for (var i = 0; i < text.Length;)
{
@@ -27,10 +27,11 @@ namespace Avalonia.UnitTests
glyphIndices[index] = glyph;
- width += glyphTypeface.GetGlyphAdvance(glyph);
+ glyphCount++;
}
- return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text);
+ return new GlyphRun(glyphTypeface, fontRenderingEmSize,
+ new ReadOnlySlice(glyphIndices.AsMemory(0, glyphCount)), characters: text);
}
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
index 028caa35c6..219c7ece46 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
@@ -1,7 +1,7 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
-using Avalonia.Utility;
+using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs
index 3ed5cfb0b2..3d489af3a2 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs
@@ -1,6 +1,6 @@
using System;
using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utility;
+using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
index 0e43c76da1..e526172622 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero()
{
- Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12));
+ Assert.Throws(() => new Typeface("foo", (FontStyle)12, 0));
}
[Fact]