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