diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
index c2914e8847..7db58e5286 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:RenderDemo.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.GlyphRunPage">
-
-
-
-
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index 7f85606957..674ed8e61f 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -9,14 +9,6 @@ namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
- private Image _imageControl;
- private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
- private readonly Random _rand = new Random();
- private ushort[] _glyphIndices = new ushort[1];
- private char[] _characters = new char[1];
- private float _fontSize = 20;
- private int _direction = 10;
-
public GlyphRunPage()
{
this.InitializeComponent();
@@ -25,19 +17,43 @@ namespace RenderDemo.Pages
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
+ }
+ }
+
+ public class GlyphRunControl : Control
+ {
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private char[] _characters = new char[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
- _imageControl = this.FindControl("imageControl");
- _imageControl.Source = new DrawingImage();
+ private DispatcherTimer _timer;
- DispatcherTimer.Run(() =>
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromSeconds(1)
+ };
+
+ _timer.Tick += (s,e) =>
{
- UpdateGlyphRun();
+ InvalidateVisual();
+ };
+
+ _timer.Start();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer.Stop();
- return true;
- }, TimeSpan.FromSeconds(1));
+ _timer = null;
}
- private void UpdateGlyphRun()
+ public override void Render(DrawingContext context)
{
var c = (char)_rand.Next(65, 90);
@@ -57,27 +73,70 @@ namespace RenderDemo.Pages
_characters[0] = c;
- var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+ var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
- var drawingGroup = new DrawingGroup();
+ context.DrawGlyphRun(Brushes.Black, glyphRun);
+ }
+ }
- var glyphRunDrawing = new GlyphRunDrawing
+ public class GlyphRunGeometryControl : Control
+ {
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private char[] _characters = new char[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
+
+ private DispatcherTimer _timer;
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer = new DispatcherTimer
{
- Foreground = Brushes.Black,
- GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
+ Interval = TimeSpan.FromSeconds(1)
};
- drawingGroup.Children.Add(glyphRunDrawing);
-
- var geometryDrawing = new GeometryDrawing
+ _timer.Tick += (s, e) =>
{
- Pen = new Pen(Brushes.Black),
- Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
+ InvalidateVisual();
};
- drawingGroup.Children.Add(geometryDrawing);
+ _timer.Start();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _timer.Stop();
+
+ _timer = null;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ var c = (char)_rand.Next(65, 90);
+
+ if (_fontSize + _direction > 200)
+ {
+ _direction = -10;
+ }
+
+ if (_fontSize + _direction < 20)
+ {
+ _direction = 10;
+ }
+
+ _fontSize += _direction;
+
+ _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+ _characters[0] = c;
+
+ var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
+
+ var geometry = glyphRun.BuildGeometry();
- (_imageControl.Source as DrawingImage).Drawing = drawingGroup;
+ context.DrawGeometry(Brushes.Green, null, geometry);
}
}
}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 22be8d8865..25c35a28e5 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -194,6 +194,25 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Obtains geometry for the glyph run.
+ ///
+ /// The geometry returned contains the combined geometry of all glyphs in the glyph run.
+ public Geometry BuildGeometry()
+ {
+ var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
+
+ var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale);
+
+ var geometry = new PlatformGeometry(geometryImpl);
+
+ var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale);
+
+ geometry.Transform = transform;
+
+ return geometry;
+ }
+
///
/// Retrieves the offset from the leading edge of the
/// to the leading or trailing edge of a caret stop containing the specified character hit.
diff --git a/src/Avalonia.Base/Media/PlatformGeometry.cs b/src/Avalonia.Base/Media/PlatformGeometry.cs
new file mode 100644
index 0000000000..f25a14540f
--- /dev/null
+++ b/src/Avalonia.Base/Media/PlatformGeometry.cs
@@ -0,0 +1,24 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+ internal class PlatformGeometry : Geometry
+ {
+ private readonly IGeometryImpl _geometryImpl;
+
+ public PlatformGeometry(IGeometryImpl geometryImpl)
+ {
+ _geometryImpl = geometryImpl;
+ }
+
+ public override Geometry Clone()
+ {
+ return new PlatformGeometry(_geometryImpl);
+ }
+
+ protected override IGeometryImpl? CreateDefiningGeometry()
+ {
+ return _geometryImpl;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index 0eeefddf0b..bfa9e70fce 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
@@ -58,6 +58,14 @@ namespace Avalonia.Platform
/// A combined geometry.
IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
+ ///
+ /// Created a geometry implementation for the glyph run.
+ ///
+ /// The glyph run to build a geometry from.
+ /// The scaling of the produces geometry.
+ /// The geometry returned contains the combined geometry of all glyphs in the glyph run.
+ IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale);
+
///
/// Creates a renderer.
///
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index addc248d58..6471b87bfd 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -114,6 +114,13 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub();
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ scale = Matrix.Identity;
+
+ return new HeadlessGeometryStub(new Rect(glyphRun.Size));
+ }
+
class HeadlessGeometryStub : IGeometryImpl
{
public HeadlessGeometryStub(Rect bounds)
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index d3c3585cd0..727677c82e 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -62,6 +62,41 @@ namespace Avalonia.Skia
return new CombinedGeometryImpl(combineMode, g1, g2);
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+ {
+ throw new InvalidOperationException("PlatformImpl can't be null.");
+ }
+
+ var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
+ var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
+ {
+ Size = fontRenderingEmSize,
+ Edging = SKFontEdging.Antialias,
+ Hinting = SKFontHinting.None,
+ LinearMetrics = true
+ };
+
+ SKPath path = new SKPath();
+ var matrix = SKMatrix.Identity;
+
+ var currentX = 0f;
+
+ foreach (var glyph in glyphRun.GlyphIndices)
+ {
+ var p = skFont.GetGlyphPath(glyph);
+
+ path.AddPath(p, currentX, 0);
+
+ currentX += p.Bounds.Right;
+ }
+
+ scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
+
+ return new StreamGeometryImpl(path);
+ }
+
///
public IBitmapImpl LoadBitmap(string fileName)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index d9e992bb80..04025f92e4 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+ {
+ throw new InvalidOperationException("PlatformImpl can't be null.");
+ }
+
+ var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory);
+
+ using (var sink = pathGeometry.Open())
+ {
+ var glyphs = new short[glyphRun.GlyphIndices.Count];
+
+ for (int i = 0; i < glyphRun.GlyphIndices.Count; i++)
+ {
+ glyphs[i] = (short)glyphRun.GlyphIndices[i];
+ }
+
+ glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink);
+
+ sink.Close();
+ }
+
+ scale = Matrix.Identity;
+
+ return new StreamGeometryImpl(pathGeometry);
+ }
+
///
public IBitmapImpl LoadBitmap(string fileName)
{
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
index add8f7fd73..183177495a 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
@@ -121,6 +121,11 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ throw new NotImplementedException();
+ }
+
class MockStreamGeometry : IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 5cbb3b2c49..51e75b6611 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -117,6 +117,11 @@ namespace Avalonia.Benchmarks
return new NullGlyphRun();
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ throw new NotImplementedException();
+ }
+
public bool SupportsIndividualRoundRects => true;
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..6a8884a33a
--- /dev/null
+++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+ public class GlyphRunTests : TestBase
+ {
+ public GlyphRunTests()
+ : base(@"Media\GlyphRun")
+ {
+ }
+
+ [Fact]
+ public async Task Should_Render_GlyphRun_Geometry()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 100,
+ Child = new GlyphRunGeometryControl
+ {
+ [TextElement.ForegroundProperty] = new LinearGradientBrush
+ {
+ StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
+ EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
+ GradientStops =
+ {
+ new GradientStop { Color = Colors.Red, Offset = 0 },
+ new GradientStop { Color = Colors.Blue, Offset = 1 }
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+
+ CompareImages();
+ }
+
+ public class GlyphRunGeometryControl : Control
+ {
+ private readonly Geometry _geometry;
+
+ public GlyphRunGeometryControl()
+ {
+ var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
+
+ var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') };
+
+ var characters = new[] { 'A', 'B', 'C' };
+
+ var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices);
+
+ _geometry = glyphRun.BuildGeometry();
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ return _geometry.Bounds.Size;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ var foreground = TextElement.GetForeground(this);
+
+ context.DrawGeometry(foreground, null, _geometry);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 376121c269..c385e1c3eb 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -122,6 +122,13 @@ namespace Avalonia.UnitTests
return Mock.Of();
}
+ public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+ {
+ scale = Matrix.Identity;
+
+ return Mock.Of();
+ }
+
public bool SupportsIndividualRoundRects { get; set; }
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
new file mode 100644
index 0000000000..7f1e0d29a1
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
new file mode 100644
index 0000000000..a8f3aa9277
Binary files /dev/null and b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ