diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
index b626eaeb68..893cb0074c 100644
--- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
+++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
@@ -12,7 +12,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 86be80b3e7..ab157f8062 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -129,18 +129,30 @@ namespace Avalonia.Headless
Point baselineOrigin,
Rect bounds)
{
- return new HeadlessGlyphRunStub();
+ return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
}
- private class HeadlessGlyphRunStub : IGlyphRunImpl
+ internal class HeadlessGlyphRunStub : IGlyphRunImpl
{
- public Rect Bounds => new Rect(new Size(8, 12));
+ public HeadlessGlyphRunStub(
+ IGlyphTypeface glyphTypeface,
+ double fontRenderingEmSize,
+ Point baselineOrigin,
+ Rect bounds)
+ {
+ GlyphTypeface = glyphTypeface;
+ FontRenderingEmSize = fontRenderingEmSize;
+ BaselineOrigin = baselineOrigin;
+ Bounds =bounds;
+ }
- public Point BaselineOrigin => new Point(0, 8);
+ public Rect Bounds { get; }
- public IGlyphTypeface GlyphTypeface => new HeadlessGlyphTypefaceImpl();
+ public Point BaselineOrigin { get; }
- public double FontRenderingEmSize => 12;
+ public IGlyphTypeface GlyphTypeface { get; }
+
+ public double FontRenderingEmSize { get; }
public void Dispose()
{
@@ -235,8 +247,11 @@ namespace Avalonia.Headless
private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
+ private HeadlessStreamingGeometryContextStub _context;
+
public HeadlessStreamingGeometryStub() : base(default)
{
+ _context = new HeadlessStreamingGeometryContextStub(this);
}
public IStreamGeometryImpl Clone()
@@ -246,13 +261,18 @@ namespace Avalonia.Headless
public IStreamGeometryContextImpl Open()
{
- return new HeadlessStreamingGeometryContextStub(this);
+ return _context;
+ }
+
+ public override bool FillContains(Point point)
+ {
+ return _context.FillContains(point);
}
private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl
{
private readonly HeadlessStreamingGeometryStub _parent;
- private double _x1, _y1, _x2, _y2;
+ private List points = new List();
public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent)
{
_parent = parent;
@@ -260,19 +280,30 @@ namespace Avalonia.Headless
private void Track(Point pt)
{
- if (_x1 > pt.X)
- _x1 = pt.X;
- if (_x2 < pt.X)
- _x2 = pt.X;
- if (_y1 > pt.Y)
- _y1 = pt.Y;
- if (_y2 < pt.Y)
- _y2 = pt.Y;
+ points.Add(pt);
}
+ public Rect CalculateBounds()
+ {
+ var left = double.MaxValue;
+ var right = double.MinValue;
+ var top = double.MaxValue;
+ var bottom = double.MinValue;
+
+ foreach (var p in points)
+ {
+ left = Math.Min(p.X, left);
+ right = Math.Max(p.X, right);
+ top = Math.Min(p.Y, top);
+ bottom = Math.Max(p.Y, bottom);
+ }
+
+ return new Rect(new Point(left, top), new Point(right, bottom));
+ }
+
public void Dispose()
{
- _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1);
+ _parent.Bounds = CalculateBounds();
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
@@ -304,6 +335,35 @@ namespace Avalonia.Headless
{
}
+
+ public bool FillContains(Point point)
+ {
+ // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
+ // to determine if the point is in the geometry (since it will always be convex in this situation)
+ for (int i = 0; i < points.Count; i++)
+ {
+ var a = points[i];
+ var b = points[(i + 1) % points.Count];
+ var c = points[(i + 2) % points.Count];
+
+ Vector v0 = c - a;
+ Vector v1 = b - a;
+ Vector v2 = point - a;
+
+ var dot00 = v0 * v0;
+ var dot01 = v0 * v1;
+ var dot02 = v0 * v2;
+ var dot11 = v1 * v1;
+ var dot12 = v1 * v2;
+
+
+ var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
+ var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+ var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+ if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
+ }
+ return false;
+ }
}
}
@@ -369,7 +429,7 @@ namespace Avalonia.Headless
}
}
- private class HeadlessDrawingContextStub : IDrawingContextImpl
+ internal class HeadlessDrawingContextStub : IDrawingContextImpl
{
public void Dispose()
{
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
index 769fea7c6e..471ea9a6d0 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
@@ -11,6 +13,7 @@ using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
@@ -82,22 +85,22 @@ namespace Avalonia.Headless
{
public FontMetrics Metrics => new FontMetrics
{
- DesignEmHeight = 1,
- Ascent = 8,
- Descent = 4,
+ DesignEmHeight = 10,
+ Ascent = 2,
+ Descent = 10,
+ IsFixedPitch = true,
LineGap = 0,
UnderlinePosition = 2,
UnderlineThickness = 1,
StrikethroughPosition = 2,
- StrikethroughThickness = 1,
- IsFixedPitch = true
+ StrikethroughThickness = 1
};
public int GlyphCount => 1337;
- public FontSimulations FontSimulations { get; }
+ public FontSimulations FontSimulations => FontSimulations.None;
- public string FamilyName => "Arial";
+ public string FamilyName => "$Default";
public FontWeight Weight => FontWeight.Normal;
@@ -111,24 +114,31 @@ namespace Avalonia.Headless
public ushort GetGlyph(uint codepoint)
{
- return 1;
+ return (ushort)codepoint;
}
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
- glyph = 1;
+ glyph = 8;
return true;
}
public int GetGlyphAdvance(ushort glyph)
{
- return 12;
+ return 8;
}
public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
{
- return glyphs.ToArray().Select(x => (int)x).ToArray();
+ var advances = new int[glyphs.Length];
+
+ for (var i = 0; i < advances.Length; i++)
+ {
+ advances[i] = 8;
+ }
+
+ return advances;
}
public ushort[] GetGlyphs(ReadOnlySpan codepoints)
@@ -146,8 +156,8 @@ namespace Avalonia.Headless
{
metrics = new GlyphMetrics
{
- Height = 10,
- Width = 8
+ Width = 10,
+ Height = 10
};
return true;
@@ -161,40 +171,81 @@ namespace Avalonia.Headless
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
+ var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+ var textSpan = text.Span;
+ var textStartIndex = TextTestHelper.GetStartCharIndex(text);
+
+ for (var i = 0; i < shapedBuffer.Length;)
+ {
+ var glyphCluster = i + textStartIndex;
+
+ var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
+
+ var glyphIndex = typeface.GetGlyph(codepoint);
- return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+ for (var j = 0; j < count; ++j)
+ {
+ shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
+ }
+
+ i += count;
+ }
+
+ return shapedBuffer;
}
}
internal class HeadlessFontManagerStub : IFontManagerImpl
{
+ private readonly string _defaultFamilyName;
+
+ public HeadlessFontManagerStub(string defaultFamilyName = "Default")
+ {
+ _defaultFamilyName = defaultFamilyName;
+ }
+
+ public int TryCreateGlyphTypefaceCount { get; private set; }
+
public string GetDefaultFontFamilyName()
{
- return "Arial";
+ return _defaultFamilyName;
}
- public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
- return new string[] { "Arial" };
+ return new[] { _defaultFamilyName };
}
- public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
+ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
+ FontStretch fontStretch,
+ CultureInfo? culture, out Typeface fontKey)
{
- glyphTypeface= new HeadlessGlyphTypefaceImpl();
+ fontKey = new Typeface(_defaultFamilyName);
- return true;
+ return false;
}
- public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
+ public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+ FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
- glyphTypeface = new HeadlessGlyphTypefaceImpl();
+ glyphTypeface = null;
+
+ TryCreateGlyphTypefaceCount++;
+
+ if (familyName == "Unknown")
+ {
+ return false;
+ }
+
+ glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
- public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface)
+ public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
- typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch);
+ glyphTypeface = new HeadlessGlyphTypefaceImpl();
+
return true;
}
}
@@ -249,4 +300,14 @@ namespace Avalonia.Headless
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
+
+ internal static class TextTestHelper
+ {
+ public static int GetStartCharIndex(ReadOnlyMemory text)
+ {
+ if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
+ throw new InvalidOperationException("text memory should have been a string");
+ return start;
+ }
+ }
}
diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
index 3ccec872d2..adb5431ce6 100644
--- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@@ -27,7 +28,7 @@ namespace Avalonia.Base.UnitTests.Media
[Fact]
public void Should_Throw_When_Default_FamilyName_Is_Null()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new HeadlessFontManagerStub(null!))))
{
Assert.Throws(() => FontManager.Current);
}
@@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests.Media
var options = new FontManagerOptions { DefaultFamilyName = "MyFont" };
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
- .With(fontManagerImpl: new MockFontManagerImpl())))
+ .With(fontManagerImpl: new HeadlessFontManagerStub())))
{
AvaloniaLocator.CurrentMutable.Bind().ToConstant(options);
@@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Media
};
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
- .With(fontManagerImpl: new MockFontManagerImpl())))
+ .With(fontManagerImpl: new HeadlessFontManagerStub())))
{
AvaloniaLocator.CurrentMutable.Bind().ToConstant(options);
diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
index 84ce341e98..c273cc6489 100644
--- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Headless;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
@@ -179,13 +180,13 @@ namespace Avalonia.Base.UnitTests.Media
glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
}
- return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
+ return new GlyphRun(new HeadlessGlyphTypefaceImpl(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
}
private static IDisposable Start()
{
return UnitTestApplication.Start(TestServices.StyledWindow.With(
- renderInterface: new MockPlatformRenderInterface()));
+ renderInterface: new HeadlessPlatformRenderInterface()));
}
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
index 27bb0355e6..7cd02d2907 100644
--- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Avalonia.Base.UnitTests.VisualTree;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
deleted file mode 100644
index 37adb03628..0000000000
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ /dev/null
@@ -1,285 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.UnitTests;
-using Avalonia.Media.Imaging;
-using Avalonia.Media.TextFormatting;
-
-namespace Avalonia.Base.UnitTests.VisualTree
-{
- class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext
- {
- public IRenderTarget CreateRenderTarget(IEnumerable