Browse Source

Implement GlyphRun.BuildGeometry

Benedikt Stebner 3 years ago
parent
commit
7a533700d0

+ 6 - 6
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">
-  <Border
+  <Grid
+    ColumnDefinitions="*,*"
     Background="White">
-    <Image
-      x:Name="imageControl"
-      Stretch="None">
-    </Image>
-  </Border>
+    <local:GlyphRunControl Grid.Column="0"/>
+    <local:GlyphRunGeometryControl Grid.Column="1"/>
+  </Grid>
 </UserControl>

+ 86 - 27
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<Image>("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);          
         }
     }
 }

+ 19 - 0
src/Avalonia.Base/Media/GlyphRun.cs

@@ -194,6 +194,25 @@ namespace Avalonia.Media
             }
         }
 
+        /// <summary>
+        /// Obtains geometry for the glyph run.
+        /// </summary>
+        /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
+        public Geometry BuildGeometry()
+        {
+            var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
+
+            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;
+        }
+
         /// <summary>
         /// Retrieves the offset from the leading edge of the <see cref="GlyphRun"/>
         /// to the leading or trailing edge of a caret stop containing the specified character hit.

+ 24 - 0
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;
+        }
+    }
+}

+ 8 - 0
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -58,6 +58,14 @@ namespace Avalonia.Platform
         /// <returns>A combined geometry.</returns>
         IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
 
+        /// <summary>
+        /// Created a geometry implementation for the glyph run.
+        /// </summary>
+        /// <param name="glyphRun">The glyph run to build a geometry from.</param>
+        /// <param name="scale">The scaling of the produces geometry.</param>
+        /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
+        IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale);
+
         /// <summary>
         /// Creates a renderer.
         /// </summary>

+ 32 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -62,6 +62,38 @@ 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 glyphs = glyphRun.GlyphIndices.ToArray();
+            var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
+            {
+                Size = fontRenderingEmSize,
+                Edging = SKFontEdging.Antialias,
+                Hinting = SKFontHinting.None,
+                LinearMetrics = true
+            };
+
+            SKPath path = null;
+            var matrix = SKMatrix.Identity;
+
+            skFont.GetGlyphPaths(glyphs, (p, m) =>
+            {
+                matrix = m;
+
+                path = p;
+            });
+
+            scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
+
+            return new StreamGeometryImpl(path);
+        }
+
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {

+ 28 - 0
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1
         public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> 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);
+        }      
+
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {

+ 66 - 0
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+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 = 80,
+                Height = 90,
+                Child = new GlyphRunGeometryControl()
+            };
+
+            await RenderToFile(target);
+
+            CompareImages();
+        }
+
+        public class GlyphRunGeometryControl : Control
+        {
+            private readonly Geometry _geometry;
+
+            public GlyphRunGeometryControl()
+            {
+                var glyphTypeface = Typeface.Default.GlyphTypeface;
+
+                var glyphIndices = new[] { glyphTypeface.GetGlyph('A') };
+
+                var characters = new[] { 'A' };
+
+                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)
+            {
+                context.DrawGeometry(Brushes.Green, null, _geometry);
+            }
+        }
+    }
+}

BIN
tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png


BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png