Browse Source

Merge pull request #8313 from Gillibald/feater/GlyphRunBuildGeometry

Feature GlyphRun.BuildGeometry
pull/8328/head
Max Katz 4 years ago
committed by GitHub
parent
commit
d67e212da1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  2. 113
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  3. 19
      src/Avalonia.Base/Media/GlyphRun.cs
  4. 24
      src/Avalonia.Base/Media/PlatformGeometry.cs
  5. 8
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  6. 7
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  7. 35
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  8. 28
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  9. 5
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  10. 5
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  11. 81
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  12. 7
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  13. BIN
      tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
  14. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

12
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>

113
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
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
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
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>

7
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)

35
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);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{

28
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)
{

5
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();

5
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;

81
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);
}
}
}
}

7
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -122,6 +122,13 @@ namespace Avalonia.UnitTests
return Mock.Of<IGlyphRunImpl>();
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
{
scale = Matrix.Identity;
return Mock.Of<IGeometryImpl>();
}
public bool SupportsIndividualRoundRects { get; set; }
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save