diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index f8767c7599..873048ef21 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 796bd8e596..08a9aa3ceb 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 7f63e7725f..b17520a466 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -41,6 +41,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
new file mode 100644
index 0000000000..fb3e318a0e
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
new file mode 100644
index 0000000000..7f15845596
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -0,0 +1,80 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace RenderDemo.Pages
+{
+ public class GlyphRunPage : UserControl
+ {
+ private DrawingPresenter _drawingPresenter;
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
+
+ public GlyphRunPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _drawingPresenter = this.FindControl("drawingPresenter");
+
+ DispatcherTimer.Run(() =>
+ {
+ UpdateGlyphRun();
+
+ return true;
+ }, TimeSpan.FromSeconds(1));
+ }
+
+ private void UpdateGlyphRun()
+ {
+ var c = (uint)_rand.Next(65, 90);
+
+ if (_fontSize + _direction > 200)
+ {
+ _direction = -10;
+ }
+
+ if (_fontSize + _direction < 20)
+ {
+ _direction = 10;
+ }
+
+ _fontSize += _direction;
+
+ _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+ var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+
+ var drawingGroup = new DrawingGroup();
+
+ var glyphRunDrawing = new GlyphRunDrawing
+ {
+ Foreground = Brushes.Black,
+ GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
+ BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
+ };
+
+ drawingGroup.Children.Add(glyphRunDrawing);
+
+ var geometryDrawing = new GeometryDrawing
+ {
+ Pen = new Pen(Brushes.Black),
+ Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
+ };
+
+ drawingGroup.Children.Add(geometryDrawing);
+
+ _drawingPresenter.Drawing = drawingGroup;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs
new file mode 100644
index 0000000000..978a5b0c4c
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/CharacterHit.cs
@@ -0,0 +1,68 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents information about a character hit within a glyph run.
+ ///
+ ///
+ /// The CharacterHit structure provides information about the index of the first
+ /// character that got hit as well as information about leading or trailing edge.
+ ///
+ public readonly struct CharacterHit : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the structure.
+ ///
+ /// Index of the first character that got hit.
+ /// In the case of a leading edge, this value is 0. In the case of a trailing edge,
+ /// this value is the number of code points until the next valid caret position.
+ public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
+ {
+ FirstCharacterIndex = firstCharacterIndex;
+
+ TrailingLength = trailingLength;
+ }
+
+ ///
+ /// Gets the index of the first character that got hit.
+ ///
+ public int FirstCharacterIndex { get; }
+
+ ///
+ /// Gets the trailing length value for the character that got hit.
+ ///
+ public int TrailingLength { get; }
+
+ public bool Equals(CharacterHit other)
+ {
+ return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is CharacterHit other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return FirstCharacterIndex * 397 ^ TrailingLength;
+ }
+ }
+
+ public static bool operator ==(CharacterHit left, CharacterHit right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(CharacterHit left, CharacterHit right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index 8aa0bac41a..df69ab6fd5 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -187,6 +187,22 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground brush.
+ /// The glyph run.
+ /// The baseline origin of the glyph run.
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ Contract.Requires(glyphRun != null);
+
+ if (foreground != null)
+ {
+ PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin);
+ }
+ }
+
///
/// Draws a filled rectangle.
///
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
new file mode 100644
index 0000000000..a5e70ae2b1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -0,0 +1,459 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using Avalonia.Platform;
+using Avalonia.Utility;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
+ ///
+ public sealed class GlyphRun : IDisposable
+ {
+ private static readonly IPlatformRenderInterface s_platformRenderInterface =
+ AvaloniaLocator.Current.GetService();
+
+ private IGlyphRunImpl _glyphRunImpl;
+ private GlyphTypeface _glyphTypeface;
+ private double _fontRenderingEmSize;
+ private Rect? _bounds;
+
+ private ReadOnlySlice _glyphIndices;
+ private ReadOnlySlice _glyphAdvances;
+ private ReadOnlySlice _glyphOffsets;
+ private ReadOnlySlice _glyphClusters;
+ private ReadOnlySlice _characters;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GlyphRun()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the class by specifying properties of the class.
+ ///
+ /// The glyph typeface.
+ /// The rendering em size.
+ /// The glyph indices.
+ /// The glyph advances.
+ /// The glyph offsets.
+ /// The characters.
+ /// The glyph clusters.
+ /// The bidi level.
+ /// The bound.
+ public GlyphRun(
+ GlyphTypeface glyphTypeface,
+ double fontRenderingEmSize,
+ ReadOnlySlice glyphIndices,
+ ReadOnlySlice glyphAdvances = default,
+ ReadOnlySlice glyphOffsets = default,
+ ReadOnlySlice characters = default,
+ ReadOnlySlice glyphClusters = default,
+ int bidiLevel = 0,
+ Rect? bounds = null)
+ {
+ GlyphTypeface = glyphTypeface;
+
+ FontRenderingEmSize = fontRenderingEmSize;
+
+ GlyphIndices = glyphIndices;
+
+ GlyphAdvances = glyphAdvances;
+
+ GlyphOffsets = glyphOffsets;
+
+ Characters = characters;
+
+ GlyphClusters = glyphClusters;
+
+ BidiLevel = bidiLevel;
+
+ Initialize(bounds);
+ }
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public GlyphTypeface GlyphTypeface
+ {
+ get => _glyphTypeface;
+ set => Set(ref _glyphTypeface, value);
+ }
+
+ ///
+ /// Gets or sets the em size used for rendering the .
+ ///
+ public double FontRenderingEmSize
+ {
+ get => _fontRenderingEmSize;
+ set => Set(ref _fontRenderingEmSize, value);
+ }
+
+ ///
+ /// Gets or sets an array of values that represent the glyph indices in the rendering physical font.
+ ///
+ public ReadOnlySlice GlyphIndices
+ {
+ get => _glyphIndices;
+ set => Set(ref _glyphIndices, value);
+ }
+
+ ///
+ /// Gets or sets an array of values that represent the advances corresponding to the glyph indices.
+ ///
+ public ReadOnlySlice GlyphAdvances
+ {
+ get => _glyphAdvances;
+ set => Set(ref _glyphAdvances, value);
+ }
+
+ ///
+ /// Gets or sets an array of values representing the offsets of the glyphs in the .
+ ///
+ public ReadOnlySlice GlyphOffsets
+ {
+ get => _glyphOffsets;
+ set => Set(ref _glyphOffsets, value);
+ }
+
+ ///
+ /// Gets or sets the list of UTF16 code points that represent the Unicode content of the .
+ ///
+ public ReadOnlySlice Characters
+ {
+ get => _characters;
+ set => Set(ref _characters, value);
+ }
+
+ ///
+ /// Gets or sets a list of values representing a mapping from character index to glyph index.
+ ///
+ public ReadOnlySlice GlyphClusters
+ {
+ get => _glyphClusters;
+ set => Set(ref _glyphClusters, value);
+ }
+
+ ///
+ /// Gets or sets the bidirectional nesting level of the .
+ ///
+ public int BidiLevel
+ {
+ get;
+ set;
+ }
+
+ ///
+ ///
+ ///
+ internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
+
+ ///
+ ///
+ ///
+ internal bool IsLeftToRight => ((BidiLevel & 1) == 0);
+
+ ///
+ /// Gets or sets the conservative bounding box of the .
+ ///
+ public Rect Bounds
+ {
+ get
+ {
+ if (_bounds == null)
+ {
+ _bounds = CalculateBounds();
+ }
+
+ return _bounds.Value;
+ }
+ set => _bounds = value;
+ }
+
+ public IGlyphRunImpl GlyphRunImpl
+ {
+ get
+ {
+ if (_glyphRunImpl == null)
+ {
+ Initialize(null);
+ }
+
+ return _glyphRunImpl;
+ }
+ }
+
+ public double GetDistanceFromCharacterHit(CharacterHit characterHit)
+ {
+ var distance = 0.0;
+
+ var end = _glyphClusters.AsSpan().BinarySearch((ushort)characterHit.FirstCharacterIndex);
+
+ if (end < 0)
+ {
+ return 0;
+ }
+
+ // If TrailingLength > 0 we have to use the next cluster while TrailingLength != 0
+ for (var i = 0; i < end + characterHit.TrailingLength; i++)
+ {
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[i];
+
+ distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ distance += GlyphAdvances[i];
+ }
+ }
+
+ return distance;
+ }
+
+ public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
+ {
+ // Before
+ if (distance < 0)
+ {
+ isInside = false;
+
+ var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _);
+
+ return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
+ }
+
+ //After
+ if (distance > Bounds.Size.Width)
+ {
+ isInside = false;
+
+ var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _);
+
+ return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
+ }
+
+ //Within
+ var currentX = 0.0;
+ var index = 0;
+
+ for (; index < GlyphIndices.Length; index++)
+ {
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[index];
+
+ currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ currentX += GlyphAdvances[index];
+ }
+
+ if (currentX > distance)
+ {
+ break;
+ }
+ }
+
+ if (index == GlyphIndices.Length)
+ {
+ index--;
+ }
+
+ var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
+
+ isInside = distance < currentX && width > 0;
+
+ var isTrailing = distance > currentX - width / 2;
+
+ return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
+ }
+
+ public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
+ {
+
+ if (characterHit.TrailingLength == 0)
+ {
+ return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
+ }
+
+ var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+
+ return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
+ }
+
+ public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
+ {
+ return characterHit.TrailingLength == 0 ?
+ FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) :
+ new CharacterHit(characterHit.FirstCharacterIndex);
+ }
+
+ private class ReverseComparer : IComparer
+ {
+ public int Compare(T x, T y)
+ {
+ return Comparer.Default.Compare(y, x);
+ }
+ }
+
+ private static readonly IComparer s_ascendingComparer = Comparer.Default;
+ private static readonly IComparer s_descendingComparer = new ReverseComparer();
+
+ internal CharacterHit FindNearestCharacterHit(int index, out double width)
+ {
+ width = 0.0;
+
+ if (index < 0)
+ {
+ return default;
+ }
+
+ var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
+
+ var clusters = _glyphClusters.AsSpan();
+
+ int start;
+
+ if (index == 0 && clusters[0] == 0)
+ {
+ start = 0;
+ }
+ else
+ {
+ // Find the start of the cluster at the character index.
+ start = clusters.BinarySearch((ushort)index, comparer);
+ }
+
+ // No cluster found.
+ if (start < 0)
+ {
+ while (index > 0 && start < 0)
+ {
+ index--;
+
+ start = clusters.BinarySearch((ushort)index, comparer);
+ }
+
+ if (start < 0)
+ {
+ return default;
+ }
+ }
+
+ var trailingLength = 0;
+
+ var currentCluster = clusters[start];
+
+ while (start > 0 && clusters[start - 1] == currentCluster)
+ {
+ start--;
+ }
+
+ for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex)
+ {
+ if (_glyphClusters[lastIndex] != currentCluster)
+ {
+ break;
+ }
+
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[lastIndex];
+
+ width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ width += GlyphAdvances[lastIndex];
+ }
+
+ trailingLength++;
+ }
+
+ return new CharacterHit(currentCluster, trailingLength);
+ }
+
+ private Rect CalculateBounds()
+ {
+ var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
+
+ var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
+
+ var width = 0.0;
+
+ if (GlyphAdvances.IsEmpty)
+ {
+ foreach (var glyph in GlyphIndices)
+ {
+ width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ }
+ else
+ {
+ foreach (var advance in GlyphAdvances)
+ {
+ width += advance;
+ }
+ }
+
+ return new Rect(0, 0, width, height);
+ }
+
+ private void Set(ref T field, T value)
+ {
+ if (_glyphRunImpl != null)
+ {
+ throw new InvalidOperationException("GlyphRun can't be changed after is has been initialized.'");
+ }
+
+ field = value;
+ }
+
+ private void Initialize(Rect? bounds)
+ {
+ if (GlyphIndices.Length == 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var glyphCount = GlyphIndices.Length;
+
+ if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount)
+ {
+ throw new InvalidOperationException();
+ }
+
+ _glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width);
+
+ if (bounds.HasValue)
+ {
+ _bounds = bounds;
+ }
+ else
+ {
+ var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
+
+ _bounds = new Rect(0, 0, width, height);
+ }
+ }
+
+ void IDisposable.Dispose()
+ {
+ _glyphRunImpl?.Dispose();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
new file mode 100644
index 0000000000..22d6e20b34
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
@@ -0,0 +1,50 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media
+{
+ public class GlyphRunDrawing : Drawing
+ {
+ public static readonly StyledProperty ForegroundProperty =
+ AvaloniaProperty.Register(nameof(Foreground));
+
+ public static readonly StyledProperty GlyphRunProperty =
+ AvaloniaProperty.Register(nameof(GlyphRun));
+
+ public static readonly StyledProperty BaselineOriginProperty =
+ AvaloniaProperty.Register(nameof(BaselineOrigin));
+
+ public IBrush Foreground
+ {
+ get => GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public GlyphRun GlyphRun
+ {
+ get => GetValue(GlyphRunProperty);
+ set => SetValue(GlyphRunProperty, value);
+ }
+
+ public Point BaselineOrigin
+ {
+ get => GetValue(BaselineOriginProperty);
+ set => SetValue(BaselineOriginProperty, value);
+ }
+
+ public override void Draw(DrawingContext context)
+ {
+ if (GlyphRun == null)
+ {
+ return;
+ }
+
+ context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
+ }
+
+ public override Rect GetBounds()
+ {
+ return GlyphRun?.Bounds ?? default;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
index b03cf5908a..6468f701d6 100644
--- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -66,6 +66,11 @@ namespace Avalonia.Media
///
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
+ ///
+ /// A value indicating whether all glyphs in the font have the same advancement.
+ ///
+ public bool IsFixedPitch => PlatformImpl.IsFixedPitch;
+
///
/// Returns an glyph index for the specified codepoint.
///
diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index 5edb1c9760..f2309c271d 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -86,6 +86,14 @@ namespace Avalonia.Platform
/// The text.
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground.
+ /// The glyph run.
+ /// The baseline origin of the glyph run.
+ void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin);
+
///
/// Creates a new that can be used as a render layer
/// for the current render target.
diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
new file mode 100644
index 0000000000..0f1359794a
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
@@ -0,0 +1,12 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// Actual implementation of a glyph run that stores platform dependent resources.
+ ///
+ public interface IGlyphRunImpl : IDisposable { }
+}
diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
index 8c043a5129..5d6ff23c0a 100644
--- a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
@@ -47,6 +47,11 @@ namespace Avalonia.Platform
///
int StrikethroughThickness { get; }
+ ///
+ /// A value indicating whether all glyphs in the font have the same advancement.
+ ///
+ bool IsFixedPitch { get; }
+
///
/// Returns an glyph index for the specified codepoint.
///
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index edde10358c..7ae0eaf8f2 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -117,5 +117,13 @@ namespace Avalonia.Platform
///
/// The font manager.
IFontManagerImpl CreateFontManager();
+
+ ///
+ /// Creates a platform implementation of a glyph run.
+ ///
+ /// The glyph run.
+ /// The glyph run's width.
+ ///
+ IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
}
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 4fbfb02660..a169a629be 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -190,6 +190,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
+ {
+ Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground)));
+ }
+
+ else
+ {
+ ++_drawOperationindex;
+ }
+ }
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
new file mode 100644
index 0000000000..b862dc218f
--- /dev/null
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
@@ -0,0 +1,91 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+ ///
+ /// A node in the scene graph which represents a text draw.
+ ///
+ internal class GlyphRunNode : BrushDrawOperation
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The transform.
+ /// The foreground brush.
+ /// The glyph run to draw.
+ /// The baseline origin of the glyph run.
+ /// Child scenes for drawing visual brushes.
+ public GlyphRunNode(
+ Matrix transform,
+ IBrush foreground,
+ GlyphRun glyphRun,
+ Point baselineOrigin,
+ IDictionary childScenes = null)
+ : base(glyphRun.Bounds, transform, null)
+ {
+ Transform = transform;
+ Foreground = foreground?.ToImmutable();
+ GlyphRun = glyphRun;
+ BaselineOrigin = baselineOrigin;
+ ChildScenes = childScenes;
+ }
+
+ ///
+ /// Gets the transform with which the node will be drawn.
+ ///
+ public Matrix Transform { get; }
+
+ ///
+ /// Gets the foreground brush.
+ ///
+ public IBrush Foreground { get; }
+
+ ///
+ /// Gets the text to draw.
+ ///
+ public GlyphRun GlyphRun { get; }
+
+ ///
+ /// Gets the baseline origin.
+ ///
+ public Point BaselineOrigin { get; set; }
+
+ ///
+ public override IDictionary ChildScenes { get; }
+
+ ///
+ public override void Render(IDrawingContextImpl context)
+ {
+ context.Transform = Transform;
+ context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
+ }
+
+ ///
+ /// Determines if this draw operation equals another.
+ ///
+ /// The transform of the other draw operation.
+ /// The foreground of the other draw operation.
+ /// The text of the other draw operation.
+ /// True if the draw operations are the same, otherwise false.
+ ///
+ /// The properties of the other draw operation are passed in as arguments to prevent
+ /// allocation of a not-yet-constructed draw operation object.
+ ///
+ internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun)
+ {
+ return transform == Transform &&
+ Equals(foreground, Foreground) &&
+ Equals(glyphRun, GlyphRun);
+ }
+
+ ///
+ public override bool HitTest(Point p) => Bounds.Contains(p);
+ }
+}
diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
new file mode 100644
index 0000000000..c54ccc8ef1
--- /dev/null
+++ b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
@@ -0,0 +1,154 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Utilities;
+
+namespace Avalonia.Utility
+{
+ ///
+ /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
+ ///
+ /// The type of elements in the slice.
+ public readonly struct ReadOnlySlice : IReadOnlyList
+ {
+ public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { }
+
+ public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length)
+ {
+ Buffer = buffer;
+ Start = start;
+ Length = length;
+ }
+
+ ///
+ /// Gets the start.
+ ///
+ ///
+ /// The start.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Gets the end.
+ ///
+ ///
+ /// The end.
+ ///
+ public int End => Start + Length - 1;
+
+ ///
+ /// Gets the length.
+ ///
+ ///
+ /// The length.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets a value that indicates whether this instance of is Empty.
+ ///
+ public bool IsEmpty => Length == 0;
+
+ ///
+ /// The buffer.
+ ///
+ public ReadOnlyMemory Buffer { get; }
+
+ public T this[int index] => Buffer.Span[Start + index];
+
+ ///
+ /// Returns a span of the underlying buffer.
+ ///
+ /// The of the underlying buffer.
+ public ReadOnlySpan AsSpan()
+ {
+ return Buffer.Span.Slice(Start, Length);
+ }
+
+ ///
+ /// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
+ ///
+ /// The start of the sub slice.
+ /// The length of the sub slice.
+ /// A that contains the specified number of elements from the specified start.
+ public ReadOnlySlice AsSlice(int start, int length)
+ {
+ if (start < 0 || start >= Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+
+ if (Start + start > End)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, Start + start, length);
+ }
+
+ ///
+ /// 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 ReadOnlySlice Take(int length)
+ {
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, 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 ReadOnlySlice Skip(int length)
+ {
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, Start + length, Length - length);
+ }
+
+ ///
+ /// Returns an enumerator for the slice.
+ ///
+ public ImmutableReadOnlyListStructEnumerator GetEnumerator()
+ {
+ return new ImmutableReadOnlyListStructEnumerator(this);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ int IReadOnlyCollection.Count => Length;
+
+ T IReadOnlyList.this[int index] => this[index];
+
+ public static implicit operator ReadOnlySlice(T[] array)
+ {
+ return new ReadOnlySlice(array);
+ }
+
+ public static implicit operator ReadOnlySlice(ReadOnlyMemory memory)
+ {
+ return new ReadOnlySlice(memory);
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 835c377791..d06cfa69a7 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -232,6 +232,20 @@ namespace Avalonia.Skia
}
}
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
+ {
+ var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
+
+ paint.ApplyTo(glyphRunImpl.Paint);
+
+ Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
+ (float)baselineOrigin.Y, glyphRunImpl.Paint);
+ }
+ }
+
///
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
new file mode 100644
index 0000000000..e0f62d6085
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ public class GlyphRunImpl : IGlyphRunImpl
+ {
+ public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
+ {
+ Paint = paint;
+ TextBlob = textBlob;
+ }
+
+ ///
+ /// Gets the paint to draw with.
+ ///
+ public SKPaint Paint { get; }
+
+ ///
+ /// Gets the text blob to draw.
+ ///
+ public SKTextBlob TextBlob { get; }
+
+ void IDisposable.Dispose()
+ {
+ TextBlob.Dispose();
+ Paint.Dispose();
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index d4dc70e808..bb2650a5c6 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -61,6 +61,8 @@ namespace Avalonia.Skia
{
StrikethroughThickness = strikethroughThickness;
}
+
+ IsFixedPitch = Typeface.IsFixedPitch;
}
public Face Face { get; }
@@ -93,6 +95,9 @@ namespace Avalonia.Skia
///
public int StrikethroughThickness { get; }
+ ///
+ public bool IsFixedPitch { get; }
+
///
public ushort GetGlyph(uint codepoint)
{
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index e17d6fdce3..05c3bbdaa0 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -156,5 +156,90 @@ namespace Avalonia.Skia
{
return new FontManagerImpl();
}
+
+ ///
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ var count = glyphRun.GlyphIndices.Length;
+
+ var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
+
+ var typeface = glyphTypeface.Typeface;
+
+ var paint = new SKPaint
+ {
+ TextSize = (float)glyphRun.FontRenderingEmSize,
+ Typeface = typeface,
+ TextEncoding = SKTextEncoding.GlyphId,
+ IsAntialias = true,
+ IsStroke = false,
+ SubpixelText = true
+ };
+
+ using (var textBlobBuilder = new SKTextBlobBuilder())
+ {
+ var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
+
+ if (glyphRun.GlyphOffsets.IsEmpty)
+ {
+ width = 0;
+
+ var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
+
+ if (!glyphTypeface.IsFixedPitch)
+ {
+ var positions = buffer.GetPositionSpan();
+
+ for (var i = 0; i < count; i++)
+ {
+ positions[i] = (float)width;
+
+ if (glyphRun.GlyphAdvances.IsEmpty)
+ {
+ width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ }
+ else
+ {
+ width += glyphRun.GlyphAdvances[i];
+ }
+ }
+ }
+
+ buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
+ }
+ else
+ {
+ var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
+
+ var glyphPositions = buffer.GetPositionSpan();
+
+ var currentX = 0.0;
+
+ for (var i = 0; i < count; i++)
+ {
+ var glyphOffset = glyphRun.GlyphOffsets[i];
+
+ glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+
+ if (glyphRun.GlyphAdvances.IsEmpty)
+ {
+ currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ }
+ else
+ {
+ currentX += glyphRun.GlyphAdvances[i];
+ }
+ }
+
+ buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
+
+ width = currentX;
+ }
+
+ var textBlob = textBlobBuilder.Build();
+
+ return new GlyphRunImpl(paint, textBlob);
+ }
+ }
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 4068b31c9a..a2bedf3190 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Platform;
+using SharpDX.DirectWrite;
+using GlyphRun = Avalonia.Media.GlyphRun;
+using TextAlignment = Avalonia.Media.TextAlignment;
namespace Avalonia
{
@@ -196,5 +199,52 @@ namespace Avalonia.Direct2D1
{
return new FontManagerImpl();
}
+
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
+
+ var glyphCount = glyphRun.GlyphIndices.Length;
+
+ var run = new SharpDX.DirectWrite.GlyphRun
+ {
+ FontFace = glyphTypeface.FontFace,
+ FontSize = (float)glyphRun.FontRenderingEmSize
+ };
+
+ var indices = new short[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ indices[i] = (short)glyphRun.GlyphIndices[i];
+ }
+
+ run.Indices = indices;
+
+ run.Advances = new float[glyphCount];
+
+ width = 0;
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ run.Advances[i] = (float)glyphRun.GlyphAdvances[i];
+ width += run.Advances[i];
+ }
+
+ run.Offsets = new GlyphOffset[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var offset = glyphRun.GlyphOffsets[i];
+
+ run.Offsets[i] = new GlyphOffset
+ {
+ AdvanceOffset = (float)offset.X,
+ AscenderOffset = (float)offset.Y
+ };
+ }
+
+ return new GlyphRunImpl(run);
+ }
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index 628f245ae5..aa13003643 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -316,6 +316,22 @@ namespace Avalonia.Direct2D1.Media
}
}
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground.
+ /// The glyph run.
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
+ {
+ var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
+
+ _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural);
+ }
+ }
+
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
if (_layerFactory != null)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
new file mode 100644
index 0000000000..0b06d5ef3e
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
@@ -0,0 +1,19 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Direct2D1.Media
+{
+ internal class GlyphRunImpl : IGlyphRunImpl
+ {
+ public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun)
+ {
+ GlyphRun = glyphRun;
+ }
+
+ public SharpDX.DirectWrite.GlyphRun GlyphRun { get; }
+
+ public void Dispose()
+ {
+ GlyphRun?.Dispose();
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
index 32def01c39..dfc3b48eaa 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
- FontFace = new FontFace(DWFont);
+ FontFace = new FontFace(DWFont).QueryInterface();
Face = new Face(GetTable);
@@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media
{
StrikethroughThickness = strikethroughThickness;
}
+
+ IsFixedPitch = FontFace.IsMonospacedFont;
}
private Blob GetTable(Face face, Tag tag)
@@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media
public SharpDX.DirectWrite.Font DWFont { get; }
- public FontFace FontFace { get; }
+ public FontFace1 FontFace { get; }
public Face Face { get; }
@@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media
///
public int StrikethroughThickness { get; }
+ ///
+ public bool IsFixedPitch { get; }
+
///
public ushort GetGlyph(uint codepoint)
{
diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
new file mode 100644
index 0000000000..93ff84d04a
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class MockGlyphTypeface : IGlyphTypefaceImpl
+ {
+ public short DesignEmHeight => 10;
+ public int Ascent => 100;
+ public int Descent => 0;
+ public int LineGap { get; }
+ public int UnderlinePosition { get; }
+ public int UnderlineThickness { get; }
+ public int StrikethroughPosition { get; }
+ public int StrikethroughThickness { get; }
+ public bool IsFixedPitch { get; }
+
+ public ushort GetGlyph(uint codepoint)
+ {
+ return 0;
+ }
+
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ return new ushort[codepoints.Length];
+ }
+
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return 100;
+ }
+
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var advances = new int[glyphs.Length];
+
+ for (var i = 0; i < advances.Length; i++)
+ {
+ advances[i] = 100;
+ }
+
+ return advances;
+ }
+
+ public void Dispose() { }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index ba436405ce..5da9f8ff6e 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -83,5 +83,11 @@ namespace Avalonia.UnitTests
{
return new MockFontManagerImpl();
}
+
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ width = 0;
+ return Mock.Of();
+ }
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..c0820f2046
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
@@ -0,0 +1,112 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class GlyphRunTests : TestWithServicesBase
+ {
+ public GlyphRunTests()
+ {
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToSingleton();
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 2, 0, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)]
+ [Theory]
+ public void Should_Get_TextBounds_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
+ int trailingLengthExpected, bool isInsideExpected)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters))
+ {
+ var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
+
+ Assert.Equal(start, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(trailingLengthExpected, textBounds.TrailingLength);
+
+ Assert.Equal(isInsideExpected, isInside);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
+ [Theory]
+ public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
+ int index, int expectedIndex, int expectedLength, double expectedWidth)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
+
+ Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(expectedLength, textBounds.TrailingLength);
+
+ Assert.Equal(expectedWidth, width, 2);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)]
+ [Theory]
+ public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int nextIndex, int nextLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(nextLength, characterHit.TrailingLength);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
+ [Theory]
+ public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int previousIndex, int previousLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(previousLength, characterHit.TrailingLength);
+ }
+ }
+
+ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
+ {
+ var count = glyphAdvances.Length;
+ var glyphIndices = new ushort[count];
+
+ var bounds = new Rect(0, 0, count * 10, 10);
+
+ return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
+ glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 300c6e359e..28304b674b 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -51,7 +51,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}