diff --git a/Avalonia.sln b/Avalonia.sln
index 4e7b4cc318..0354e20d4f 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -60,20 +60,17 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
EndProject
-Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
EndProject
-Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}"
@@ -235,15 +232,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "sampl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
+EndProject
Global
- GlobalSection(SharedMSBuildProjectFiles) = preSolution
- src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
- src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
- src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
- src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
- src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
- src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
- EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
Ad-Hoc|iPhone = Ad-Hoc|iPhone
@@ -2169,6 +2160,30 @@ Global
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhone.Build.0 = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2187,11 +2202,9 @@ Global
{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C}
- {E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {3C4C0CB4-0C0F-4450-A37B-148C84FF905F} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props
index fff00041c3..3fccad2641 100644
--- a/build/CoreLibraries.props
+++ b/build/CoreLibraries.props
@@ -17,5 +17,6 @@
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index 7d75901288..7f24ef35bc 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -3,7 +3,7 @@
Avalonia
0.10.999
- Copyright 2021 © The AvaloniaUI Project
+ Copyright 2022 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
true
diff --git a/build/SourceLink.props b/build/SourceLink.props
index 1e007e01eb..9f05848881 100644
--- a/build/SourceLink.props
+++ b/build/SourceLink.props
@@ -3,7 +3,6 @@
true
false
true
- embedded
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
@@ -15,6 +14,10 @@
true
+
+ embedded
+
+
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 0b88908252..40180274e1 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -1627,6 +1627,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return;
}
}
+ else if (type == Magnify)
+ {
+ delta.X = delta.Y = [event magnification];
+ }
+ else if (type == Rotate)
+ {
+ delta.X = delta.Y = [event rotation];
+ }
+ else if (type == Swipe)
+ {
+ delta.X = [event deltaX];
+ delta.Y = [event deltaY];
+ }
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
@@ -1753,6 +1766,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super scrollWheel:event];
}
+- (void)magnifyWithEvent:(NSEvent *)event
+{
+ [self mouseEvent:event withType:Magnify];
+ [super magnifyWithEvent:event];
+}
+
+- (void)rotateWithEvent:(NSEvent *)event
+{
+ [self mouseEvent:event withType:Rotate];
+ [super rotateWithEvent:event];
+}
+
+- (void)swipeWithEvent:(NSEvent *)event
+{
+ [self mouseEvent:event withType:Swipe];
+ [super swipeWithEvent:event];
+}
+
- (void)mouseEntered:(NSEvent *)event
{
_isMouseOver = true;
diff --git a/samples/ControlCatalog.Android/Assets/AboutAssets.txt b/samples/ControlCatalog.Android/Assets/AboutAssets.txt
index ee39886295..a9b0638eb1 100644
--- a/samples/ControlCatalog.Android/Assets/AboutAssets.txt
+++ b/samples/ControlCatalog.Android/Assets/AboutAssets.txt
@@ -1,7 +1,7 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories) and given a Build Action of "AndroidAsset".
-These files will be deployed with you package and will be accessible using Android's
+These files will be deployed with your package and will be accessible using Android's
AssetManager, like this:
public class ReadAsset : Activity
@@ -16,4 +16,4 @@ public class ReadAsset : Activity
Additionally, some Android functions will automatically load asset files:
-Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");
\ No newline at end of file
+Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");
diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
index d463dfa84a..199fa85ad2 100644
--- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
+++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
@@ -2,6 +2,7 @@
net6.0
enable
+ True
diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 227b31bf20..d0e1bd885e 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -3,6 +3,7 @@
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
+ Name="Avalonia ControlCatalog"
x:Class="ControlCatalog.App">
-
+
+
+
+
diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs
index 5357181838..c5b669343a 100644
--- a/samples/RenderDemo/Pages/ClippingPage.xaml.cs
+++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs
@@ -1,35 +1,18 @@
-using System;
-using System.Reactive.Linq;
-using Avalonia;
-using Avalonia.Animation;
using Avalonia.Controls;
-using Avalonia.Data;
using Avalonia.Markup.Xaml;
-using Avalonia.Media;
namespace RenderDemo.Pages
{
public class ClippingPage : UserControl
{
- private Geometry _clip;
-
public ClippingPage()
{
InitializeComponent();
- WireUpCheckbox();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
-
- private void WireUpCheckbox()
- {
- var useMask = this.FindControl("useMask");
- var clipped = this.FindControl("clipped");
- _clip = clipped.Clip;
- useMask.Click += (s, e) => clipped.Clip = clipped.Clip == null ? _clip : null;
- }
}
}
diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs
index 2e59d934a1..9c524a7932 100644
--- a/samples/RenderDemo/Pages/CustomSkiaPage.cs
+++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@@ -41,7 +42,10 @@ namespace RenderDemo.Pages
{
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
- context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl);
+ using (var c = new DrawingContext(context, false))
+ {
+ c.DrawText(_noSkia, new Point());
+ }
else
{
canvas.Save();
@@ -108,10 +112,9 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
- var noSkia = new FormattedText()
- {
- Text = "Current rendering API is not Skia"
- };
+ var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
+
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
}
diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml b/samples/RenderDemo/Pages/FormattedTextPage.axaml
new file mode 100644
index 0000000000..92775bec9e
--- /dev/null
+++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml
@@ -0,0 +1,7 @@
+
+
diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
new file mode 100644
index 0000000000..25e29c67a9
--- /dev/null
+++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
@@ -0,0 +1,60 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+
+namespace RenderDemo.Pages
+{
+ public class FormattedTextPage : UserControl
+ {
+ public FormattedTextPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor";
+
+ // Create the initial formatted text string.
+ var formattedText = new FormattedText(
+ testString,
+ CultureInfo.GetCultureInfo("en-us"),
+ FlowDirection.LeftToRight,
+ new Typeface("Verdana"),
+ 32,
+ Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 };
+
+ // Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
+
+ // Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
+ // The font size is calculated in terms of points -- not as device-independent pixels.
+ formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5);
+
+ // Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
+ formattedText.SetFontWeight(FontWeight.Bold, 6, 11);
+
+ var gradient = new LinearGradientBrush
+ {
+ GradientStops =
+ new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) },
+ StartPoint = new RelativePoint(0,0, RelativeUnit.Relative),
+ EndPoint = new RelativePoint(0,1, RelativeUnit.Relative)
+ };
+
+ // Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
+ formattedText.SetForegroundBrush(gradient, 6, 11);
+
+ // Use an Italic font style beginning at the 28th character and continuing for 28 characters.
+ formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
+
+ context.DrawText(formattedText, new Point(10, 0));
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index 857358f6b2..7f85606957 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -13,6 +13,7 @@ namespace RenderDemo.Pages
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;
@@ -38,7 +39,7 @@ namespace RenderDemo.Pages
private void UpdateGlyphRun()
{
- var c = (uint)_rand.Next(65, 90);
+ var c = (char)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
@@ -54,6 +55,8 @@ namespace RenderDemo.Pages
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+ _characters[0] = c;
+
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup();
@@ -61,7 +64,7 @@ namespace RenderDemo.Pages
var glyphRunDrawing = new GlyphRunDrawing
{
Foreground = Brushes.Black,
- GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
+ GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
};
drawingGroup.Children.Add(glyphRunDrawing);
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 6a940a54f1..2d4f6a305f 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -10,7 +10,7 @@ using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
-using Avalonia.Shared.PlatformSupport;
+using Avalonia.PlatformSupport;
using Avalonia.Skia;
namespace Avalonia
@@ -59,8 +59,7 @@ namespace Avalonia.Android
.Bind().ToSingleton()
.Bind().ToConstant(new ChoreographerTimer())
.Bind().ToConstant(new RenderLoop())
- .Bind().ToSingleton()
- .Bind().ToConstant(new AssetLoader(appType.Assembly));
+ .Bind().ToSingleton();
SkiaPlatform.Initialize();
diff --git a/src/Android/Avalonia.Android/AppBuilder.cs b/src/Android/Avalonia.Android/AppBuilder.cs
index 805bb61655..04f1ff00d0 100644
--- a/src/Android/Avalonia.Android/AppBuilder.cs
+++ b/src/Android/Avalonia.Android/AppBuilder.cs
@@ -1,5 +1,5 @@
using Avalonia.Controls;
-using Avalonia.Shared.PlatformSupport;
+using Avalonia.PlatformSupport;
namespace Avalonia
{
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index 8c6775733f..5c33dbcea6 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -5,9 +5,11 @@
+
+ TargetFramework=netstandard2.0
+
-
diff --git a/src/Android/Avalonia.Android/RuntimeInfo.cs b/src/Android/Avalonia.Android/RuntimeInfo.cs
deleted file mode 100644
index bb2466c357..0000000000
--- a/src/Android/Avalonia.Android/RuntimeInfo.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Avalonia.Platform;
-
-namespace Avalonia.Shared.PlatformSupport
-{
- internal partial class StandardRuntimePlatform
- {
- public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo
- {
- IsCoreClr = false,
- IsDesktop = false,
- IsMobile = true,
- IsDotNetFramework = false,
- IsMono = true,
- IsUnix = true,
- OperatingSystem = OperatingSystemType.Android
- };
- }
-}
\ No newline at end of file
diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 4811028f85..50fc5ac73b 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -157,7 +157,7 @@ namespace Avalonia.Animation
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
- Clock ?? AvaloniaLocator.Current.GetService(),
+ Clock ?? AvaloniaLocator.Current.GetRequiredService(),
oldValue,
newValue);
return;
@@ -169,7 +169,7 @@ namespace Avalonia.Animation
base.OnPropertyChangedCore(change);
}
- private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!_transitionsEnabled)
{
@@ -179,14 +179,14 @@ namespace Avalonia.Animation
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- AddTransitions(e.NewItems);
+ AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
- RemoveTransitions(e.OldItems);
+ RemoveTransitions(e.OldItems!);
break;
case NotifyCollectionChangedAction.Replace:
- RemoveTransitions(e.OldItems);
- AddTransitions(e.NewItems);
+ RemoveTransitions(e.OldItems!);
+ AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Transitions collection cannot be reset.");
@@ -204,7 +204,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
- var t = (ITransition)items[i];
+ var t = (ITransition)items[i]!;
_transitionState.Add(t, new TransitionState
{
@@ -222,7 +222,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
- var t = (ITransition)items[i];
+ var t = (ITransition)items[i]!;
if (_transitionState.TryGetValue(t, out var state))
{
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index a4515db514..03b2d17e44 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -203,7 +203,7 @@ namespace Avalonia.Animation
///
/// The animation setter.
/// The property animator type.
- public static Type GetAnimator(IAnimationSetter setter)
+ public static Type? GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
@@ -254,7 +254,7 @@ namespace Avalonia.Animation
Animators.Insert(0, (condition, typeof(TAnimator)));
}
- private static Type GetAnimatorType(AvaloniaProperty property)
+ private static Type? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type) in Animators)
{
@@ -276,6 +276,11 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
+ if (setter.Property is null)
+ {
+ throw new InvalidOperationException("No Setter property assigned.");
+ }
+
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
@@ -305,7 +310,7 @@ namespace Avalonia.Animation
foreach (var (handlerType, property) in handlerList)
{
- var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
+ var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!;
newInstance.Property = property;
newAnimatorInstances.Add(newInstance);
}
@@ -321,32 +326,43 @@ namespace Avalonia.Animation
}
///
- public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete)
+ public IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
- subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
+ var subscription = animators[0].Apply(this, control, clock, match, onComplete);
+
+ if (subscription is not null)
+ {
+ subscriptions.Add(subscription);
+ }
}
else
{
var completionTasks = onComplete != null ? new List() : null;
foreach (IAnimator animator in animators)
{
- Action animatorOnComplete = null;
+ Action? animatorOnComplete = null;
if (onComplete != null)
{
- var tcs = new TaskCompletionSource
@@ -16,6 +17,5 @@
-
diff --git a/src/Avalonia.DesktopRuntime/RuntimeInfo.cs b/src/Avalonia.DesktopRuntime/RuntimeInfo.cs
deleted file mode 100644
index 82eaadb895..0000000000
--- a/src/Avalonia.DesktopRuntime/RuntimeInfo.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Avalonia.Platform;
-
-
-namespace Avalonia.Shared.PlatformSupport
-{
- internal partial class StandardRuntimePlatform
- {
- private static readonly Lazy Info = new Lazy(() =>
- {
- OperatingSystemType os;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- os = OperatingSystemType.OSX;
- else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- os = OperatingSystemType.Linux;
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- os = OperatingSystemType.WinNT;
- else
- throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
-
- return new RuntimePlatformInfo
- {
-#if NETCOREAPP2_0
- IsCoreClr = true,
-#elif NET461
- IsDotNetFramework = false,
-#endif
- IsDesktop = true,
- IsMono = false,
- IsMobile = false,
- IsUnix = os != OperatingSystemType.WinNT,
- OperatingSystem = os,
- };
- });
-
-
- public RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
- }
-}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 45bfe5ff81..c32638f6ca 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:diag="clr-namespace:Avalonia.Diagnostics"
+ xmlns:default="using:Avalonia.Themes.Default"
Title="Avalonia DevTools"
x:Class="Avalonia.Diagnostics.Views.MainWindow">
@@ -9,8 +10,7 @@
-
-
+
diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml
index 5f18bac44a..1843abebfd 100644
--- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml
+++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml
@@ -1,13 +1,9 @@
-
diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
index 30c6d39856..6247815303 100644
--- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
+++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
@@ -1,82 +1,38 @@
-
diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
index 678f75b43f..ef200b5532 100644
--- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
+++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj
@@ -13,10 +13,7 @@
-
-
-
-
+
diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/AutoCompleteBox.xaml
rename to src/Avalonia.Themes.Default/Controls/AutoCompleteBox.xaml
diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Controls/Button.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/Button.xaml
rename to src/Avalonia.Themes.Default/Controls/Button.xaml
diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ButtonSpinner.xaml
rename to src/Avalonia.Themes.Default/Controls/ButtonSpinner.xaml
diff --git a/src/Avalonia.Themes.Default/Calendar.xaml b/src/Avalonia.Themes.Default/Controls/Calendar.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/Calendar.xaml
rename to src/Avalonia.Themes.Default/Controls/Calendar.xaml
diff --git a/src/Avalonia.Themes.Default/CalendarButton.xaml b/src/Avalonia.Themes.Default/Controls/CalendarButton.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CalendarButton.xaml
rename to src/Avalonia.Themes.Default/Controls/CalendarButton.xaml
diff --git a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml b/src/Avalonia.Themes.Default/Controls/CalendarDatePicker.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CalendarDatePicker.xaml
rename to src/Avalonia.Themes.Default/Controls/CalendarDatePicker.xaml
diff --git a/src/Avalonia.Themes.Default/CalendarDayButton.xaml b/src/Avalonia.Themes.Default/Controls/CalendarDayButton.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CalendarDayButton.xaml
rename to src/Avalonia.Themes.Default/Controls/CalendarDayButton.xaml
diff --git a/src/Avalonia.Themes.Default/CalendarItem.xaml b/src/Avalonia.Themes.Default/Controls/CalendarItem.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CalendarItem.xaml
rename to src/Avalonia.Themes.Default/Controls/CalendarItem.xaml
diff --git a/src/Avalonia.Themes.Default/CaptionButtons.xaml b/src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CaptionButtons.xaml
rename to src/Avalonia.Themes.Default/Controls/CaptionButtons.xaml
diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Controls/Carousel.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/Carousel.xaml
rename to src/Avalonia.Themes.Default/Controls/Carousel.xaml
diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/CheckBox.xaml
rename to src/Avalonia.Themes.Default/Controls/CheckBox.xaml
diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/Controls/ComboBox.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ComboBox.xaml
rename to src/Avalonia.Themes.Default/Controls/ComboBox.xaml
diff --git a/src/Avalonia.Themes.Default/ComboBoxItem.xaml b/src/Avalonia.Themes.Default/Controls/ComboBoxItem.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ComboBoxItem.xaml
rename to src/Avalonia.Themes.Default/Controls/ComboBoxItem.xaml
diff --git a/src/Avalonia.Themes.Default/ContentControl.xaml b/src/Avalonia.Themes.Default/Controls/ContentControl.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ContentControl.xaml
rename to src/Avalonia.Themes.Default/Controls/ContentControl.xaml
diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/Controls/ContextMenu.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ContextMenu.xaml
rename to src/Avalonia.Themes.Default/Controls/ContextMenu.xaml
diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/DataValidationErrors.xaml
rename to src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/Controls/DatePicker.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/DatePicker.xaml
rename to src/Avalonia.Themes.Default/Controls/DatePicker.xaml
diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/Controls/EmbeddableControlRoot.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml
rename to src/Avalonia.Themes.Default/Controls/EmbeddableControlRoot.xaml
diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Controls/Expander.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/Expander.xaml
rename to src/Avalonia.Themes.Default/Controls/Expander.xaml
diff --git a/src/Avalonia.Themes.Default/FlyoutPresenter.xaml b/src/Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/FlyoutPresenter.xaml
rename to src/Avalonia.Themes.Default/Controls/FlyoutPresenter.xaml
diff --git a/src/Avalonia.Themes.Default/FocusAdorner.xaml b/src/Avalonia.Themes.Default/Controls/FocusAdorner.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/FocusAdorner.xaml
rename to src/Avalonia.Themes.Default/Controls/FocusAdorner.xaml
diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/Controls/GridSplitter.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/GridSplitter.xaml
rename to src/Avalonia.Themes.Default/Controls/GridSplitter.xaml
diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/Controls/ItemsControl.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/ItemsControl.xaml
rename to src/Avalonia.Themes.Default/Controls/ItemsControl.xaml
diff --git a/src/Avalonia.Themes.Default/Label.xaml b/src/Avalonia.Themes.Default/Controls/Label.xaml
similarity index 100%
rename from src/Avalonia.Themes.Default/Label.xaml
rename to src/Avalonia.Themes.Default/Controls/Label.xaml
diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/Controls/ListBox.xaml
similarity index 91%
rename from src/Avalonia.Themes.Default/ListBox.xaml
rename to src/Avalonia.Themes.Default/Controls/ListBox.xaml
index e3417aa086..b1fcb830b3 100644
--- a/src/Avalonia.Themes.Default/ListBox.xaml
+++ b/src/Avalonia.Themes.Default/Controls/ListBox.xaml
@@ -6,6 +6,7 @@
+
+ Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
+ IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs
index effdb88537..598b418977 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml.cs
@@ -1,4 +1,3 @@
-using Avalonia.Markup.Xaml;
using Avalonia.Styling;
namespace Avalonia.Themes.Default
diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs
new file mode 100644
index 0000000000..1d9f2d5f9d
--- /dev/null
+++ b/src/Avalonia.Themes.Default/SimpleTheme.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Styling;
+#nullable enable
+
+namespace Avalonia.Themes.Default
+{
+ public class SimpleTheme : AvaloniaObject, IStyle, IResourceProvider
+ {
+ public static readonly StyledProperty ModeProperty =
+ AvaloniaProperty.Register(nameof(Mode));
+
+ private readonly Uri _baseUri;
+ private bool _isLoading;
+ private IStyle? _loaded;
+ private Styles _sharedStyles = new();
+ private Styles _simpleDark = new();
+ private Styles _simpleLight = new();
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The base URL for the XAML context.
+ public SimpleTheme(Uri baseUri)
+ {
+ _baseUri = baseUri;
+ InitStyles(_baseUri);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The XAML service provider.
+ public SimpleTheme(IServiceProvider serviceProvider)
+ {
+ var service = serviceProvider.GetService(typeof(IUriContext));
+ if (service == null)
+ {
+ throw new Exception("There is no service object of type IUriContext!");
+ }
+ _baseUri = ((IUriContext)service).BaseUri;
+ InitStyles(_baseUri);
+ }
+
+ public event EventHandler OwnerChanged
+ {
+ add
+ {
+ if (Loaded is IResourceProvider rp)
+ {
+ rp.OwnerChanged += value;
+ }
+ }
+ remove
+ {
+ if (Loaded is IResourceProvider rp)
+ {
+ rp.OwnerChanged -= value;
+ }
+ }
+ }
+
+ IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty();
+
+ bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
+
+ public IStyle Loaded
+ {
+ get
+ {
+ if (_loaded == null)
+ {
+ _isLoading = true;
+
+ if (Mode == SimpleThemeMode.Light)
+ {
+ _loaded = new Styles { _sharedStyles, _simpleLight };
+ }
+ else if (Mode == SimpleThemeMode.Dark)
+ {
+ _loaded = new Styles { _sharedStyles, _simpleDark };
+ }
+ _isLoading = false;
+ }
+
+ return _loaded!;
+ }
+ }
+
+ ///
+ /// Gets or sets the mode of the fluent theme (light, dark).
+ ///
+ public SimpleThemeMode Mode
+ {
+ get => GetValue(ModeProperty);
+ set => SetValue(ModeProperty, value);
+ }
+ public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner;
+
+ void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner);
+
+ void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
+
+ public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
+
+ public bool TryGetResource(object key, out object? value)
+ {
+ if (!_isLoading && Loaded is IResourceProvider p)
+ {
+ return p.TryGetResource(key, out value);
+ }
+
+ value = null;
+ return false;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == ModeProperty)
+ {
+ if (Mode == SimpleThemeMode.Dark)
+ {
+ (Loaded as Styles)![1] = _simpleDark[0];
+ }
+ else
+ {
+ (Loaded as Styles)![1] = _simpleLight[0];
+ }
+ }
+ }
+
+ private void InitStyles(Uri baseUri)
+ {
+ _sharedStyles = new Styles
+ {
+ new StyleInclude(baseUri)
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
+ },
+ new StyleInclude(baseUri)
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/Accents/Base.xaml")
+ }
+ };
+ _simpleLight = new Styles
+ {
+ new StyleInclude(baseUri)
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")
+ }
+ };
+
+ _simpleDark = new Styles
+ {
+ new StyleInclude(baseUri)
+ {
+ Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml")
+ }
+ };
+ }
+
+ }
+}
diff --git a/src/Avalonia.Themes.Default/SimpleThemeMode.cs b/src/Avalonia.Themes.Default/SimpleThemeMode.cs
new file mode 100644
index 0000000000..be33466327
--- /dev/null
+++ b/src/Avalonia.Themes.Default/SimpleThemeMode.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.Themes.Default
+{
+ public enum SimpleThemeMode
+ {
+ Light,
+ Dark
+ }
+}
diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
index f2344ab380..836cc27db3 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
@@ -1,7 +1,8 @@
+ xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
+ x:CompileBindings="True">
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
index ca538e4b0a..3a6af60983 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
index 6c4e94caf1..ffd3972b66 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
@@ -7,7 +7,8 @@
+ xmlns:sys="clr-namespace:System;assembly=netstandard"
+ x:CompileBindings="True">
+
@@ -107,7 +109,6 @@
Padding="{TemplateBinding Padding}"
Watermark="{TemplateBinding Watermark}"
UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}"
- DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Grid.Column="0"/>
@@ -136,8 +137,12 @@
DisplayDateEnd="{TemplateBinding DisplayDateEnd}" />
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
index ff72044c93..9223c0dd71 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
index 5bf3ac11af..a9c8281cf0 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
@@ -6,7 +6,9 @@
-->
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:CompileBindings="True"
+ x:DataType="CalendarItem">
@@ -32,6 +34,7 @@
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 72c25cea37..831537f578 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -1,7 +1,9 @@
+ xmlns:sys="clr-namespace:System;assembly=netstandard"
+ x:DataType="MenuItem"
+ x:CompileBindings="True">
diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
index 799fe6ffe4..243095c004 100644
--- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
@@ -1,6 +1,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
index 3320fc9a41..9aa73fc52e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
@@ -7,7 +7,8 @@
+ xmlns:sys="clr-namespace:System;assembly=netstandard"
+ x:CompileBindings="True">
40
1
diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml
index debdfb2772..2d18be91cb 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml
@@ -1,6 +1,7 @@
+ xmlns:sys="clr-namespace:System;assembly=netstandard"
+ x:CompileBindings="True">
+
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:DataType="WindowNotificationManager"
+ x:CompileBindings="True">
";
var styles = AvaloniaRuntimeXamlLoader.Parse(xaml);
diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
deleted file mode 100644
index 7528424521..0000000000
--- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
+++ /dev/null
@@ -1,267 +0,0 @@
-using Avalonia.Media;
-using Avalonia.Platform;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using Xunit;
-
-#if AVALONIA_SKIA
-namespace Avalonia.Skia.RenderTests
-#else
-
-using Avalonia.Direct2D1.RenderTests;
-
-namespace Avalonia.Direct2D1.RenderTests.Media
-#endif
-{
- public class FormattedTextImplTests : TestBase
- {
- private const string FontName = "Courier New";
- private const double FontSize = 12;
- private const double MediumFontSize = 18;
- private const double BigFontSize = 32;
- private const double FontSizeHeight = 13.594;//real value 13.59375
- private const string stringword = "word";
- private const string stringmiddle = "The quick brown fox jumps over the lazy dog";
- private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog";
- private const string stringmiddle3lines = "01234567\n\n0123456789";
- private const string stringmiddlenewlines = "012345678\r 1234567\r\n 12345678\n0123456789";
-
- private const string stringlong =
-"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis " +
-"aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero" +
-" at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus " +
-"pretium ornare est.";
-
- public FormattedTextImplTests()
- : base(@"Media\FormattedText")
- {
- }
-
- private IFormattedTextImpl Create(string text,
- string fontFamily,
- double fontSize,
- FontStyle fontStyle,
- TextAlignment textAlignment,
- FontWeight fontWeight,
- TextWrapping wrapping,
- double widthConstraint)
- {
- var r = AvaloniaLocator.Current.GetService();
- return r.CreateFormattedText(text,
- new Typeface(fontFamily, fontStyle, fontWeight),
- fontSize,
- textAlignment,
- wrapping,
- widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
- null);
- }
-
- private IFormattedTextImpl Create(string text, double fontSize)
- {
- return Create(text, FontName, fontSize,
- FontStyle.Normal, TextAlignment.Left,
- FontWeight.Normal, TextWrapping.NoWrap,
- -1);
- }
-
- private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
- {
- return Create(text, FontName, fontSize,
- FontStyle.Normal, alignment,
- FontWeight.Normal, TextWrapping.NoWrap,
- widthConstraint);
- }
-
- private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
- {
- return Create(text, FontName, fontSize,
- FontStyle.Normal, TextAlignment.Left,
- FontWeight.Normal, wrap,
- widthConstraint);
- }
-
-
- [Theory]
- [InlineData("", FontSize, 0, FontSizeHeight)]
- [InlineData("x", FontSize, 7.20, FontSizeHeight)]
- [InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
- [InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
- [InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)]
- [InlineData(stringmiddle, BigFontSize, 825.73, 36.25)]
- [InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
- [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)]
- [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)]
- [InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
- [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
- public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
- {
- var fmt = Create(input, fontSize);
- var size = fmt.Bounds.Size;
-
- Assert.Equal(expWidth, size.Width, 2);
- Assert.Equal(expHeight, size.Height, 2);
-
- var linesHeight = fmt.GetLines().Sum(l => l.Height);
-
- Assert.Equal(expHeight, linesHeight, 2);
- }
-
- [Theory]
- [InlineData("", 1, -1, TextWrapping.NoWrap)]
- [InlineData("x", 1, -1, TextWrapping.NoWrap)]
- [InlineData(stringword, 1, -1, TextWrapping.NoWrap)]
- [InlineData(stringmiddle, 1, -1, TextWrapping.NoWrap)]
- [InlineData(stringmiddle, 3, 150, TextWrapping.Wrap)]
- [InlineData(stringmiddle2lines, 2, -1, TextWrapping.NoWrap)]
- [InlineData(stringmiddle2lines, 3, 150, TextWrapping.Wrap)]
- [InlineData(stringlong, 1, -1, TextWrapping.NoWrap)]
- [InlineData(stringlong, 18, 150, TextWrapping.Wrap)]
- [InlineData(stringmiddlenewlines, 4, -1, TextWrapping.NoWrap)]
- [InlineData(stringmiddlenewlines, 4, 150, TextWrapping.Wrap)]
- public void Should_Break_Lines_String_Correctly(string input,
- int linesCount,
- double widthConstraint,
- TextWrapping wrap)
- {
- var fmt = Create(input, FontSize, wrap, widthConstraint);
- var constrained = fmt;
-
- var lines = constrained.GetLines().ToArray();
- Assert.Equal(linesCount, lines.Count());
- }
-
- [Theory]
- [InlineData("x", 0, 0, true, false, 0)]
- [InlineData(stringword, -1, -1, false, false, 0)]
- [InlineData(stringword, 25, 13, true, false, 3)]
- [InlineData(stringword, 28.70, 13.5, true, true, 3)]
- [InlineData(stringword, 30, 13, false, true, 3)]
- [InlineData(stringword + "\r\n", 30, 13, false, false, 4)]
- [InlineData(stringword + "\r\nnext", 30, 13, false, false, 4)]
- [InlineData(stringword, 300, 13, false, true, 3)]
- [InlineData(stringword + "\r\n", 300, 13, false, false, 4)]
- [InlineData(stringword + "\r\nnext", 300, 13, false, false, 4)]
- [InlineData(stringword, 300, 300, false, true, 3)]
- //TODO: Direct2D implementation return textposition 6
- //but the text is 6 length, can't find the logic for me it should be 5
- //[InlineData(stringword + "\r\n", 300, 300, false, false, 6)]
- [InlineData(stringword + "\r\nnext", 300, 300, false, true, 9)]
- [InlineData(stringword + "\r\nnext", 300, 25, false, true, 9)]
- [InlineData(stringword, 28, 15, false, true, 3)]
- [InlineData(stringword, 30, 15, false, true, 3)]
- [InlineData(stringmiddle3lines, 30, 15, false, false, 9)]
- [InlineData(stringmiddle3lines, 500, 13, false, false, 8)]
- [InlineData(stringmiddle3lines, 30, 25, false, false, 9)]
- [InlineData(stringmiddle3lines, -1, 30, false, false, 10)]
- public void Should_HitTestPoint_Correctly(string input,
- double x, double y,
- bool isInside, bool isTrailing, int pos)
- {
- var fmt = Create(input, FontSize);
- var htRes = fmt.HitTestPoint(new Point(x, y));
-
- Assert.Equal(pos, htRes.TextPosition);
- Assert.Equal(isInside, htRes.IsInside);
- Assert.Equal(isTrailing, htRes.IsTrailing);
- }
-
- [Theory]
- [InlineData("", 0, 0, 0, 0, FontSizeHeight)]
- [InlineData("x", 0, 0, 0, 7.20, FontSizeHeight)]
- [InlineData("x", -1, 7.20, 0, 0, FontSizeHeight)]
- [InlineData(stringword, 3, 21.60, 0, 7.20, FontSizeHeight)]
- [InlineData(stringword, 4, 21.60 + 7.20, 0, 0, FontSizeHeight)]
- [InlineData(stringmiddlenewlines, 10, 0, FontSizeHeight, 7.20, FontSizeHeight)]
- [InlineData(stringmiddlenewlines, 15, 36.01, FontSizeHeight, 7.20, FontSizeHeight)]
- [InlineData(stringmiddlenewlines, 20, 0, 2 * FontSizeHeight, 7.20, FontSizeHeight)]
- [InlineData(stringmiddlenewlines, -1, 72.01, 3 * FontSizeHeight, 0, FontSizeHeight)]
- public void Should_HitTestPosition_Correctly(string input,
- int index, double x, double y, double width, double height)
- {
- var fmt = Create(input, FontSize);
- var r = fmt.HitTestTextPosition(index);
-
- Assert.Equal(x, r.X, 2);
- Assert.Equal(y, r.Y, 2);
- Assert.Equal(width, r.Width, 2);
- Assert.Equal(height, r.Height, 2);
- }
-
- [Theory]
- [InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
- [InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)]
- [InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
- public void Should_HitTestPosition_RigthAlign_Correctly(
- string input, int index, double widthConstraint,
- double x, double y, double width, double height)
- {
- //parse expected
- var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
- var constrained = fmt;
- var r = constrained.HitTestTextPosition(index);
-
- Assert.Equal(x, r.X, 2);
- Assert.Equal(y, r.Y, 2);
- Assert.Equal(width, r.Width, 2);
- Assert.Equal(height, r.Height, 2);
- }
-
- [Theory]
- [InlineData("x", 0, 200, 100 - 7.20 / 2, 0, 7.20, FontSizeHeight)]
- [InlineData(stringword, 0, 200, 85.6, 0, 7.20, FontSizeHeight)]
- [InlineData(stringword, 3, 200, 100 + 7.20, 0, 7.20, FontSizeHeight)]
- public void Should_HitTestPosition_CenterAlign_Correctly(
- string input, int index, double widthConstraint,
- double x, double y, double width, double height)
- {
- //parse expected
- var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
- var constrained = fmt;
- var r = constrained.HitTestTextPosition(index);
-
- Assert.Equal(x, r.X, 2);
- Assert.Equal(y, r.Y, 2);
- Assert.Equal(width, r.Width, 2);
- Assert.Equal(height, r.Height, 2);
- }
-
- [Theory]
- [InlineData("x", 0, 1, "0,0,7.20,13.59")]
- [InlineData(stringword, 0, 4, "0,0,28.80,13.59")]
- [InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")]
- [InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")]
- [InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")]
- [InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")]
- public void Should_HitTestRange_Correctly(string input,
- int index, int length,
- string expectedRects)
- {
- //parse expected result
- var rects = expectedRects.Split(';').Select(s =>
- {
- double[] v = s.Split(',')
- .Select(sd => double.Parse(sd, CultureInfo.InvariantCulture)).ToArray();
- return new Rect(v[0], v[1], v[2], v[3]);
- }).ToArray();
-
- var fmt = Create(input, FontSize);
- var htRes = fmt.HitTestTextRange(index, length).ToArray();
-
- Assert.Equal(rects.Length, htRes.Length);
-
- for (int i = 0; i < rects.Length; i++)
- {
- var exr = rects[i];
- var r = htRes[i];
-
- Assert.Equal(exr.X, r.X, 2);
- Assert.Equal(exr.Y, r.Y, 2);
- Assert.Equal(exr.Width, r.Width, 2);
- Assert.Equal(exr.Height, r.Height, 2);
- }
- }
- }
-}
diff --git a/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs
new file mode 100644
index 0000000000..fc9cbf6a7f
--- /dev/null
+++ b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs
@@ -0,0 +1,56 @@
+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 StreamGeometryTests : TestBase
+ {
+ public StreamGeometryTests()
+ : base(@"Media\StreamGeometry")
+ {
+ }
+
+ [Fact]
+ public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions()
+ {
+ var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 };
+ foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise })
+ foreach (var isLargeArc in new[] { false, true })
+ foreach (var isPrecise in new[] { false, true })
+ {
+ Point Pt(double x, double y) => new Point(x, y);
+ Size Sz(double w, double h) => new Size(w, h);
+ var streamGeometry = new StreamGeometry();
+ using (var context = streamGeometry.Open())
+ {
+ context.BeginFigure(Pt(20, 20), true);
+
+ if(isPrecise)
+ context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
+ else
+ context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
+ context.LineTo(Pt(40, 20));
+ context.LineTo(Pt(20, 20));
+ context.EndFigure(true);
+ }
+ var pathShape = new Avalonia.Controls.Shapes.Path();
+ pathShape.Data = streamGeometry;
+ pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue);
+ pathShape.Fill = new SolidColorBrush(Colors.Gold);
+ pathShape.StrokeThickness = 2;
+ pathShape.Margin = new Thickness(20);
+ grid.Children.Add(pathShape);
+ }
+ await RenderToFile(grid);
+ }
+ }
+}
diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
index b1a2d00b00..981ae0d0a4 100644
--- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
@@ -1,34 +1,265 @@
-using Avalonia.Controls;
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media;
using Avalonia.Platform;
-using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
using Xunit;
-using System.Runtime.InteropServices;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
-
-using Avalonia.Direct2D1.RenderTests;
-
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class TextLayoutTests : TestBase
{
+ private const string FontName = "Courier New";
+ private const double FontSize = 12;
+ private const double MediumFontSize = 18;
+ private const double BigFontSize = 32;
+ private const double FontSizeHeight = 13.594;//real value 13.59375
+ private const string stringword = "word";
+ private const string stringmiddle = "The quick brown fox jumps over the lazy dog";
+ private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog";
+ private const string stringmiddle3lines = "01234567\n\n0123456789";
+ private const string stringmiddlenewlines = "012345678\r 1234567\r\n 12345678\n0123456789";
+
+ private const string stringlong =
+"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis " +
+"aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero" +
+" at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus " +
+"pretium ornare est.";
+
public TextLayoutTests()
: base(@"Media\TextFormatting\TextLayout")
{
}
- [Fact]
+ private TextLayout Create(string text,
+ string fontFamily,
+ double fontSize,
+ FontStyle fontStyle,
+ TextAlignment textAlignment,
+ FontWeight fontWeight,
+ TextWrapping wrapping,
+ double widthConstraint)
+ {
+ var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
+
+ var formattedText = new TextLayout(text, typeface, fontSize, null, textAlignment, wrapping,
+ maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint);
+
+ return formattedText;
+ }
+
+ private TextLayout Create(string text, double fontSize)
+ {
+ return Create(text, FontName, fontSize,
+ FontStyle.Normal, TextAlignment.Left,
+ FontWeight.Normal, TextWrapping.NoWrap,
+ -1);
+ }
+
+ private TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
+ {
+ return Create(text, FontName, fontSize,
+ FontStyle.Normal, alignment,
+ FontWeight.Normal, TextWrapping.NoWrap,
+ widthConstraint);
+ }
+
+ private TextLayout Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
+ {
+ return Create(text, FontName, fontSize,
+ FontStyle.Normal, TextAlignment.Left,
+ FontWeight.Normal, wrap,
+ widthConstraint);
+ }
+
+
+ [Theory]
+ [InlineData("", FontSize, 0, FontSizeHeight)]
+ [InlineData("x", FontSize, 7.20, FontSizeHeight)]
+ [InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
+ [InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
+ [InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)]
+ [InlineData(stringmiddle, BigFontSize, 825.73, 36.25)]
+ [InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
+ [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)]
+ [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)]
+ [InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
+ [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
+ public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
+ {
+ var fmt = Create(input, fontSize);
+
+ Assert.Equal(expWidth, fmt.Size.Width, 2);
+ Assert.Equal(expHeight, fmt.Size.Height, 2);
+ }
+
+ [Theory]
+ [InlineData("", 1, -1, TextWrapping.NoWrap)]
+ [InlineData("x", 1, -1, TextWrapping.NoWrap)]
+ [InlineData(stringword, 1, -1, TextWrapping.NoWrap)]
+ [InlineData(stringmiddle, 1, -1, TextWrapping.NoWrap)]
+ [InlineData(stringmiddle, 3, 150, TextWrapping.Wrap)]
+ [InlineData(stringmiddle2lines, 2, -1, TextWrapping.NoWrap)]
+ [InlineData(stringmiddle2lines, 3, 150, TextWrapping.Wrap)]
+ [InlineData(stringlong, 1, -1, TextWrapping.NoWrap)]
+ [InlineData(stringlong, 18, 150, TextWrapping.Wrap)]
+ [InlineData(stringmiddlenewlines, 4, -1, TextWrapping.NoWrap)]
+ [InlineData(stringmiddlenewlines, 4, 150, TextWrapping.Wrap)]
+ public void Should_Break_Lines_String_Correctly(string input,
+ int linesCount,
+ double widthConstraint,
+ TextWrapping wrap)
+ {
+ var fmt = Create(input, FontSize, wrap, widthConstraint);
+ var constrained = fmt;
+
+ var lines = constrained.TextLines.ToArray();
+ Assert.Equal(linesCount, lines.Count());
+ }
+
+ [Theory]
+ [InlineData("x", 0, 0, true, false, 0)]
+ [InlineData(stringword, -1, -1, false, false, 0)]
+ [InlineData(stringword, 25, 13, true, false, 3)]
+ [InlineData(stringword, 28.70, 13.5, true, true, 4)]
+ [InlineData(stringword, 30, 13, false, true, 4)]
+ [InlineData(stringword + "\r\n", 30, 13, false, false, 4)]
+ [InlineData(stringword + "\r\nnext", 30, 13, false, false, 4)]
+ [InlineData(stringword, 300, 13, false, true, 4)]
+ [InlineData(stringword + "\r\n", 300, 13, false, false, 4)]
+ [InlineData(stringword + "\r\nnext", 300, 13, false, false, 4)]
+ [InlineData(stringword, 300, 300, false, true, 4)]
+ //TODO: Direct2D implementation return textposition 6
+ //but the text is 6 length, can't find the logic for me it should be 5
+ //[InlineData(stringword + "\r\n", 300, 300, false, false, 6)]
+ [InlineData(stringword + "\r\nnext", 300, 300, false, true, 10)]
+ [InlineData(stringword + "\r\nnext", 300, 25, false, true, 10)]
+ [InlineData(stringword, 28, 15, false, true, 4)]
+ [InlineData(stringword, 30, 15, false, true, 4)]
+ [InlineData(stringmiddle3lines, 30, 15, false, false, 9)]
+ [InlineData(stringmiddle3lines, 500, 13, false, false, 8)]
+ [InlineData(stringmiddle3lines, 30, 25, false, false, 9)]
+ [InlineData(stringmiddle3lines, -1, 30, false, false, 10)]
+ public void Should_HitTestPoint_Correctly(string input,
+ double x, double y,
+ bool isInside, bool isTrailing, int pos)
+ {
+ var fmt = Create(input, FontSize);
+ var htRes = fmt.HitTestPoint(new Point(x, y));
+
+ Assert.Equal(pos, htRes.TextPosition);
+ Assert.Equal(isInside, htRes.IsInside);
+ Assert.Equal(isTrailing, htRes.IsTrailing);
+ }
+
+ [Theory]
+ [InlineData("", 0, 0, 0, 0, FontSizeHeight)]
+ [InlineData("x", 0, 0, 0, 7.20, FontSizeHeight)]
+ [InlineData("x", -1, 7.20, 0, 0, FontSizeHeight)]
+ [InlineData(stringword, 3, 21.60, 0, 7.20, FontSizeHeight)]
+ [InlineData(stringword, 4, 21.60 + 7.20, 0, 0, FontSizeHeight)]
+ [InlineData(stringmiddlenewlines, 10, 0, FontSizeHeight, 7.20, FontSizeHeight)]
+ [InlineData(stringmiddlenewlines, 15, 36.01, FontSizeHeight, 7.20, FontSizeHeight)]
+ [InlineData(stringmiddlenewlines, 20, 0, 2 * FontSizeHeight, 7.20, FontSizeHeight)]
+ [InlineData(stringmiddlenewlines, -1, 72.01, 3 * FontSizeHeight, 0, FontSizeHeight)]
+ public void Should_HitTestPosition_Correctly(string input,
+ int index, double x, double y, double width, double height)
+ {
+ var fmt = Create(input, FontSize);
+ var r = fmt.HitTestTextPosition(index);
+
+ Assert.Equal(x, r.X, 2);
+ Assert.Equal(y, r.Y, 2);
+ Assert.Equal(width, r.Width, 2);
+ Assert.Equal(height, r.Height, 2);
+ }
+
+ [Theory]
+ [InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
+ [InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)]
+ [InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
+ public void Should_HitTestPosition_RigthAlign_Correctly(
+ string input, int index, double widthConstraint,
+ double x, double y, double width, double height)
+ {
+ //parse expected
+ var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
+ var constrained = fmt;
+ var r = constrained.HitTestTextPosition(index);
+
+ Assert.Equal(x, r.X, 2);
+ Assert.Equal(y, r.Y, 2);
+ Assert.Equal(width, r.Width, 2);
+ Assert.Equal(height, r.Height, 2);
+ }
+
+ [Theory]
+ [InlineData("x", 0, 200, 100 - 7.20 / 2, 0, 7.20, FontSizeHeight)]
+ [InlineData(stringword, 0, 200, 85.6, 0, 7.20, FontSizeHeight)]
+ [InlineData(stringword, 3, 200, 100 + 7.20, 0, 7.20, FontSizeHeight)]
+ public void Should_HitTestPosition_CenterAlign_Correctly(
+ string input, int index, double widthConstraint,
+ double x, double y, double width, double height)
+ {
+ //parse expected
+ var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
+ var constrained = fmt;
+ var r = constrained.HitTestTextPosition(index);
+
+ Assert.Equal(x, r.X, 2);
+ Assert.Equal(y, r.Y, 2);
+ Assert.Equal(width, r.Width, 2);
+ Assert.Equal(height, r.Height, 2);
+ }
+
+ [Theory]
+ [InlineData("x", 0, 1, "0,0,7.20,13.59")]
+ [InlineData(stringword, 0, 4, "0,0,28.80,13.59")]
+ [InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")]
+ [InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")]
+ [InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")]
+ [InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")]
+ public void Should_HitTestRange_Correctly(string input,
+ int index, int length,
+ string expectedRects)
+ {
+ //parse expected result
+ var rects = expectedRects.Split(';').Select(s =>
+ {
+ double[] v = s.Split(',')
+ .Select(sd => double.Parse(sd, CultureInfo.InvariantCulture)).ToArray();
+ return new Rect(v[0], v[1], v[2], v[3]);
+ }).ToArray();
+
+ var fmt = Create(input, FontSize);
+ var htRes = fmt.HitTestTextRange(index, length).ToArray();
+
+ Assert.Equal(rects.Length, htRes.Length);
+
+ for (int i = 0; i < rects.Length; i++)
+ {
+ var exr = rects[i];
+ var r = htRes[i];
+
+ Assert.Equal(exr.X, r.X, 2);
+ Assert.Equal(exr.Y, r.Y, 2);
+ Assert.Equal(exr.Width, r.Width, 2);
+ Assert.Equal(exr.Height, r.Height, 2);
+ }
+ }
+
+ [Fact]
public async Task TextLayout_Basic()
{
// Skip test on OSX: text rendering is subtly different.
diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs
index a0fbf704cf..b70c721085 100644
--- a/tests/Avalonia.RenderTests/TestBase.cs
+++ b/tests/Avalonia.RenderTests/TestBase.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Skia.RenderTests
namespace Avalonia.Direct2D1.RenderTests
#endif
{
- using Avalonia.Shared.PlatformSupport;
+ using Avalonia.PlatformSupport;
public class TestBase
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
index f9c45e7d22..904f0935c4 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@@ -9,30 +10,222 @@ namespace Avalonia.Skia.UnitTests.Media
{
public class GlyphRunTests
{
- [InlineData("ABC \r", 29, 4, 1)]
- [InlineData("ABC \r", 23, 3, 1)]
- [InlineData("ABC \r", 17, 2, 1)]
- [InlineData("ABC \r", 11, 1, 1)]
- [InlineData("ABC \r", 7, 1, 0)]
- [InlineData("ABC \r", 5, 0, 1)]
- [InlineData("ABC \r", 2, 0, 0)]
+ [InlineData("ABC012345", 0)] //LeftToRight
+ [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
[Theory]
- public void Should_Get_Distance_From_CharacterHit(string text, double distance, int expectedIndex,
- int expectedTrailingLength)
+ public void Should_Get_Next_CharacterHit(string text, sbyte direction)
{
using (Start())
{
- var glyphRun =
- TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default, 10, CultureInfo.CurrentCulture);
+ var shapedBuffer =
+ TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
- var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
-
- Assert.Equal(expectedIndex, characterHit.FirstCharacterIndex);
+ var glyphRun = CreateGlyphRun(shapedBuffer);
+
+ var characterHit = new CharacterHit(0);
+ var rects = BuildRects(glyphRun);
+
+ if (glyphRun.IsLeftToRight)
+ {
+ foreach (var rect in rects)
+ {
+ characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(rect.Right, distance);
+ }
+ }
+ else
+ {
+ shapedBuffer.GlyphInfos.Span.Reverse();
+
+ foreach (var rect in rects)
+ {
+ characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(rect.Left, distance);
+ }
+ }
+ }
+ }
+
+ [InlineData("ABC012345", 0)] //LeftToRight
+ [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
+ [Theory]
+ public void Should_Get_Previous_CharacterHit(string text, sbyte direction)
+ {
+ using (Start())
+ {
+ var shapedBuffer =
+ TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
+
+ var glyphRun = CreateGlyphRun(shapedBuffer);
+
+ var characterHit = new CharacterHit(text.Length);
+ var rects = BuildRects(glyphRun);
+
+ rects.Reverse();
- Assert.Equal(expectedTrailingLength, characterHit.TrailingLength);
+ if (glyphRun.IsLeftToRight)
+ {
+ foreach (var rect in rects)
+ {
+ characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(rect.Left, distance);
+ }
+ }
+ else
+ {
+ shapedBuffer.GlyphInfos.Span.Reverse();
+
+ foreach (var rect in rects)
+ {
+ characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(rect.Right, distance);
+ }
+ }
}
}
+ [InlineData("ABC012345", 0)] //LeftToRight
+ [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
+ [Theory]
+ public void Should_Get_CharacterHit_From_Distance(string text, sbyte direction)
+ {
+ using (Start())
+ {
+ var shapedBuffer =
+ TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default.GlyphTypeface, 10, CultureInfo.CurrentCulture, direction);
+
+ var glyphRun = CreateGlyphRun(shapedBuffer);
+
+ if (glyphRun.IsLeftToRight)
+ {
+ var characterHit =
+ glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _);
+
+ Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+ }
+ else
+ {
+ shapedBuffer.GlyphInfos.Span.Reverse();
+
+ var characterHit =
+ glyphRun.GetCharacterHitFromDistance(0, out _);
+
+ Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+ }
+
+ var rects = BuildRects(glyphRun);
+
+ var lastCluster = -1;
+ var index = 0;
+
+ if (!glyphRun.IsLeftToRight)
+ {
+ rects.Reverse();
+ }
+
+ foreach (var rect in rects)
+ {
+ var currentCluster = glyphRun.GlyphClusters[index];
+
+ while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count)
+ {
+ currentCluster = glyphRun.GlyphClusters[++index];
+ }
+
+ //Non trailing edge
+ var distance = glyphRun.IsLeftToRight ? rect.Left : rect.Right;
+
+ var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
+
+ Assert.Equal(currentCluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+ lastCluster = currentCluster;
+
+ index++;
+ }
+ }
+ }
+
+ private static List BuildRects(GlyphRun glyphRun)
+ {
+ var height = glyphRun.Size.Height;
+
+ var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace;
+
+ var rects = new List(glyphRun.GlyphAdvances!.Count);
+
+ var lastCluster = -1;
+
+ for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++)
+ {
+ var currentCluster = glyphRun.GlyphClusters![index];
+
+ var advance = glyphRun.GlyphAdvances[index];
+
+ if (lastCluster != currentCluster)
+ {
+ if (glyphRun.IsLeftToRight)
+ {
+ rects.Add(new Rect(currentX, 0, advance, height));
+ }
+ else
+ {
+ rects.Add(new Rect(currentX - advance, 0, advance, height));
+ }
+ }
+ else
+ {
+ var rect = rects[index - 1];
+
+ rects.Remove(rect);
+
+ rect = glyphRun.IsLeftToRight ?
+ rect.WithWidth(rect.Width + advance) :
+ new Rect(rect.X - advance, 0, rect.Width + advance, height);
+
+ rects.Add(rect);
+ }
+
+ if (glyphRun.IsLeftToRight)
+ {
+ currentX += advance;
+ }
+ else
+ {
+ currentX -= advance;
+ }
+
+ lastCluster = currentCluster;
+ }
+
+ return rects;
+ }
+
+ private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
+ {
+ return new GlyphRun(
+ shapedBuffer.GlyphTypeface,
+ shapedBuffer.FontRenderingEmSize,
+ shapedBuffer.Text,
+ shapedBuffer.GlyphIndices,
+ shapedBuffer.GlyphAdvances,
+ shapedBuffer.GlyphOffsets,
+ shapedBuffer.GlyphClusters,
+ shapedBuffer.BidiLevel);
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index a19f97e74e..326997328b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -163,8 +163,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
-
- Assert.Equal(4, textLine.TextRuns[0].Text.Length);
+
+ var firstRun = textLine.TextRuns[0];
+
+ Assert.Equal(4, firstRun.Text.Length);
}
}
@@ -204,40 +206,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(expectedNumberOfLines, numberOfLines);
}
}
-
- [Fact]
- public void Should_Wrap_RightToLeft()
- {
- using (Start())
- {
- const string text =
- "قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء";
-
- var defaultProperties = new GenericTextRunProperties(Typeface.Default);
-
- var textSource = new SingleBufferTextSource(text, defaultProperties);
-
- var formatter = new TextFormatterImpl();
-
- var currentTextSourceIndex = 0;
-
- while (currentTextSourceIndex < text.Length)
- {
- var textLine =
- formatter.FormatLine(textSource, currentTextSourceIndex, 50,
- new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
-
- var glyphClusters = textLine.TextRuns.Cast()
- .SelectMany(x => x.GlyphRun.GlyphClusters).ToArray();
-
- Assert.True(glyphClusters[0] >= glyphClusters[^1]);
-
- Assert.Equal(currentTextSourceIndex, glyphClusters[^1]);
-
- currentTextSourceIndex += textLine.TextRange.Length;
- }
- }
- }
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth."
@@ -435,28 +403,96 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [InlineData(TextAlignment.Left)]
- [InlineData(TextAlignment.Center)]
- [InlineData(TextAlignment.Right)]
+ [InlineData("0123456789", TextAlignment.Left, FlowDirection.LeftToRight)]
+ [InlineData("0123456789", TextAlignment.Center, FlowDirection.LeftToRight)]
+ [InlineData("0123456789", TextAlignment.Right, FlowDirection.LeftToRight)]
+
+ [InlineData("0123456789", TextAlignment.Left, FlowDirection.RightToLeft)]
+ [InlineData("0123456789", TextAlignment.Center, FlowDirection.RightToLeft)]
+ [InlineData("0123456789", TextAlignment.Right, FlowDirection.RightToLeft)]
+
+ [InlineData("שנבגק", TextAlignment.Left, FlowDirection.RightToLeft)]
+ [InlineData("שנבגק", TextAlignment.Center, FlowDirection.RightToLeft)]
+ [InlineData("שנבגק", TextAlignment.Right, FlowDirection.RightToLeft)]
+
[Theory]
- public void Should_Align_TextLine(TextAlignment textAlignment)
+ public void Should_Align_TextLine(string text, TextAlignment textAlignment, FlowDirection flowDirection)
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textAlignment);
+
+ var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
+ defaultProperties, TextWrapping.NoWrap, 0, 0);
- var textSource = new SingleBufferTextSource("0123456789", defaultProperties);
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 100, paragraphProperties);
- var expectedOffset = TextLine.GetParagraphOffsetX(textLine.Width, 100, textAlignment);
-
+ var expectedOffset = 0d;
+
+ if (flowDirection == FlowDirection.LeftToRight)
+ {
+ switch (textAlignment)
+ {
+ case TextAlignment.Center:
+ expectedOffset = 50 - textLine.Width / 2;
+ break;
+ case TextAlignment.Right:
+ expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
+ break;
+ }
+ }
+ else
+ {
+ switch (textAlignment)
+ {
+ case TextAlignment.Left:
+ expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
+ break;
+ case TextAlignment.Center:
+ expectedOffset = 50 - textLine.Width / 2;
+ break;
+ }
+ }
+
Assert.Equal(expectedOffset, textLine.Start);
}
}
+
+ [Fact]
+ public void Should_Wrap_Syriac()
+ {
+ using (Start())
+ {
+ const string text =
+ "܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊";
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+ var paragraphProperties =
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
+ var formatter = new TextFormatterImpl();
+
+ var textPosition = 87;
+ TextLineBreak lastBreak = null;
+
+ while (textPosition < text.Length)
+ {
+ var textLine =
+ formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
+
+ Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
+
+ textPosition += textLine.TextRange.Length;
+
+ lastBreak = textLine.TextLineBreak;
+ }
+ }
+ }
[Fact]
public void Should_FormatLine_With_Emergency_Breaks()
@@ -475,7 +511,57 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.NotNull(textLine.TextLineBreak?.RemainingCharacters);
}
}
-
+
+ [InlineData("פעילות הבינאום, W3C!")]
+ [InlineData("abcABC")]
+ [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")]
+ [InlineData("טטטט abcDEF טטטט")]
+ [Theory]
+ public void Should_Not_Alter_TextRuns_After_TextStyles_Were_Applied(string text)
+ {
+ using (Start())
+ {
+ var formatter = new TextFormatterImpl();
+
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+ var paragraphProperties =
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
+
+ var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
+
+ var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties),
+ 0, double.PositiveInfinity, paragraphProperties);
+
+ var expectedRuns = expectedTextLine.TextRuns.Cast().ToList();
+
+ var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList();
+
+ for (var i = 0; i < text.Length; i++)
+ {
+ for (var j = 1; i + j < text.Length; j++)
+ {
+ var spans = new[]
+ {
+ new ValueSpan(i, j,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
+ };
+
+ var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans);
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ var shapedRuns = textLine.TextRuns.Cast().ToList();
+
+ var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList();
+
+ Assert.Equal(expectedGlyphs, actualGlyphs);
+ }
+ }
+ }
+ }
+
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index afa1fbf461..b525931061 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@@ -13,6 +14,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
private const string SingleLineText = "0123456789";
private const string MultiLineText = "01 23 45 678\r\rabc def gh ij";
+ private const string RightToLeftText = "זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן";
[InlineData("01234\r01234\r", 3)]
[InlineData("01234\r01234", 2)]
@@ -59,7 +61,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
- var actual = textRun.Text.Buffer.Span.ToString();
+ var actual = textRun.Text.Span.ToString();
Assert.Equal("1 ", actual);
@@ -67,54 +69,127 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [Fact]
- public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
+ [InlineData(27)]
+ [InlineData(22)]
+ [Theory]
+ public void Should_Wrap_And_Apply_Style(int length)
{
using (Start())
{
+ var text = "Multiline TextBox with TextWrapping.";
+
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
var expected = new TextLayout(
- MultiLineText,
+ text,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
textWrapping: TextWrapping.Wrap,
- maxWidth: 25);
-
- var expectedLines = expected.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
+ maxWidth: 200);
+
+ var expectedLines = expected.TextLines.Select(x => text.Substring(x.TextRange.Start,
x.TextRange.Length)).ToList();
-
- for (var i = 4; i < MultiLineText.Length; i++)
+
+ var spans = new[]
{
- var spans = new[]
- {
- new ValueSpan(0, i,
- new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
- };
+ new ValueSpan(0, length,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
+ };
- var actual = new TextLayout(
- MultiLineText,
- Typeface.Default,
- 12.0f,
- Brushes.Black.ToImmutable(),
- textWrapping: TextWrapping.Wrap,
- maxWidth: 25,
- textStyleOverrides: spans);
+ var actual = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable(),
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 200,
+ textStyleOverrides: spans);
+
+ var actualLines = actual.TextLines.Select(x => text.Substring(x.TextRange.Start,
+ x.TextRange.Length)).ToList();
+
+ Assert.Equal(expectedLines.Count, actualLines.Count);
+
+ for (var j = 0; j < actual.TextLines.Count; j++)
+ {
+ var expectedText = expectedLines[j];
+
+ var actualText = actualLines[j];
+
+ Assert.Equal(expectedText, actualText);
+ }
+
+ }
+ }
+
+ [Fact]
+ public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
+ {
+ using (Start())
+ {
+ const string text = "אחד !\ntwo !\nשְׁלוֹשָׁה !";
+
+ var red = new SolidColorBrush(Colors.Red).ToImmutable();
+ var black = Brushes.Black.ToImmutable();
+
+ var expected = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ black,
+ textWrapping: TextWrapping.Wrap);
+
+ var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
- var actualLines = actual.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
- x.TextRange.Length)).ToList();
-
- Assert.Equal(expectedLines.Count, actualLines.Count);
+ var outer = new GraphemeEnumerator(text.AsMemory());
+ var inner = new GraphemeEnumerator(text.AsMemory());
+ var i = 0;
+ var j = 0;
- for (var j = 0; j < actual.TextLines.Count; j++)
+ while (true)
+ {
+ while (inner.MoveNext())
{
- var expectedText = expectedLines[j];
-
- var actualText = actualLines[j];
+ j += inner.Current.Text.Length;
+
+ if(j + i > text.Length)
+ {
+ break;
+ }
+
+ var spans = new[]
+ {
+ new ValueSpan(i, j,
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: red))
+ };
+
+ var actual = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ black,
+ textWrapping: TextWrapping.Wrap,
+ textStyleOverrides: spans);
+
+ var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+
+ Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
+
+ for (var k = 0; k < expectedGlyphs.Count; k++)
+ {
+ Assert.Equal(expectedGlyphs[k], actualGlyphs[k]);
+ }
+ }
- Assert.Equal(expectedText, actualText);
+ if (!outer.MoveNext())
+ {
+ break;
}
+
+ inner = new GraphemeEnumerator(text.AsMemory());
+
+ i += outer.Current.Text.Length;
}
}
}
@@ -184,7 +259,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
- var actual = textRun.Text.Buffer.Span.ToString();
+ var actual = textRun.Text.Span.ToString();
Assert.Equal("89", actual);
@@ -254,7 +329,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
- var actual = textRun.Text.Buffer.Span.ToString();
+ var actual = textRun.Text.Span.ToString();
Assert.Equal("😄", actual);
@@ -384,12 +459,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
-
[Theory]
- [InlineData("☝🏿", new ushort[] { 0 })]
- [InlineData("☝🏿 ab", new ushort[] { 0, 3, 4, 5 })]
- [InlineData("ab ☝🏿", new ushort[] { 0, 1, 2, 3 })]
- public void Should_Create_Valid_Clusters_For_Text(string text, ushort[] clusters)
+ [InlineData("☝🏿", new int[] { 0 })]
+ [InlineData("☝🏿 ab", new int[] { 0, 3, 4, 5 })]
+ [InlineData("ab ☝🏿", new int[] { 0, 1, 2, 3 })]
+ public void Should_Create_Valid_Clusters_For_Text(string text, int[] clusters)
{
using (Start())
{
@@ -407,15 +481,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var shapedRun = (ShapedTextCharacters)textRun;
- var glyphRun = shapedRun.GlyphRun;
-
- var glyphClusters = glyphRun.GlyphClusters;
+ var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters;
- var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
+ var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray();
- Assert.Equal(expected, glyphRun.GlyphClusters);
+ Assert.Equal(expected, glyphClusters);
- index += glyphClusters.Length;
+ index += glyphClusters.Count;
}
}
}
@@ -440,13 +512,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
- Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
+ Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count);
- Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
+ Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]);
if (expectedLength == 7)
{
- Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
+ Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]);
}
}
}
@@ -618,6 +690,161 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_HitTestTextRange_RightToLeft()
+ {
+ using (Start())
+ {
+ const int start = 0;
+ const int length = 10;
+
+ var layout = new TextLayout(
+ RightToLeftText,
+ Typeface.Default,
+ 12,
+ Brushes.Black);
+
+ var selectedText = new TextLayout(
+ RightToLeftText.Substring(start, length),
+ Typeface.Default,
+ 12,
+ Brushes.Black);
+
+ var rects = layout.HitTestTextRange(start, length).ToArray();
+
+ Assert.Equal(1, rects.Length);
+
+ var selectedRect = rects[0];
+
+ Assert.Equal(selectedText.Size.Width, selectedRect.Width);
+ }
+ }
+
+ [Fact]
+ public void Should_HitTestTextRange_BiDi()
+ {
+ const string text = "זה כיףabcDEFזה כיף";
+
+ using (Start())
+ {
+ var layout = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable());
+
+ var textLine = layout.TextLines[0];
+
+ var firstStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1));
+
+ var firstEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(0));
+
+ var secondStart = textLine.GetDistanceFromCharacterHit(new CharacterHit(6));
+
+ var secondEnd = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1));
+
+ var rects = layout.HitTestTextRange(0, 7).ToArray();
+
+ Assert.Equal(2, rects.Length);
+
+ var firstExpected = rects[0];
+
+ Assert.Equal(firstExpected.Left, firstStart);
+ Assert.Equal(firstExpected.Right, firstEnd);
+
+ var secondExpected = rects[1];
+
+ Assert.Equal(secondExpected.Left, secondStart);
+ Assert.Equal(secondExpected.Right, secondEnd);
+ }
+ }
+
+ [Fact]
+ public void Should_HitTestTextRange()
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(
+ SingleLineText,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable());
+
+ var lineRects = layout.HitTestTextRange(0, SingleLineText.Length).ToList();
+
+ Assert.Equal(layout.TextLines.Count, lineRects.Count);
+
+ for (var i = 0; i < layout.TextLines.Count; i++)
+ {
+ var textLine = layout.TextLines[i];
+ var rect = lineRects[i];
+
+ Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width);
+ }
+
+ var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast())
+ .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray();
+
+ for (var i = 0; i < SingleLineText.Length; i++)
+ {
+ for (var j = 1; i + j < SingleLineText.Length; j++)
+ {
+ var expected = rects.AsSpan(i, j).ToArray().Sum();
+ var actual = layout.HitTestTextRange(i, j).Sum(x => x.Width);
+
+ Assert.Equal(expected, actual);
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void Should_Wrap_RightToLeft()
+ {
+ const string text =
+ "يَجِبُ عَلَى الإنْسَانِ أن يَكُونَ أمِيْنَاً وَصَادِقَاً مَعَ نَفْسِهِ وَمَعَ أَهْلِهِ وَجِيْرَانِهِ وَأَنْ يَبْذُلَ كُلَّ جُهْدٍ فِي إِعْلاءِ شَأْنِ الوَطَنِ وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَن يَتِمَّ لَهُ ذلِك إِلا بِأَنْ يُقَدِّمَ المَنْفَعَةَ العَامَّةَ عَلَى المَنْفَعَةِ الخَاصَّةِ وَهذَا مِثَالٌ لِلتَّضْحِيَةِ .";
+
+ using (Start())
+ {
+ for (var maxWidth = 366; maxWidth < 900; maxWidth += 33)
+ {
+ var layout = new TextLayout(
+ text,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable(),
+ textWrapping: TextWrapping.Wrap,
+ flowDirection: FlowDirection.RightToLeft,
+ maxWidth: maxWidth);
+
+ foreach (var textLine in layout.TextLines)
+ {
+ Assert.True(textLine.Width <= maxWidth);
+
+ var actual = new string(textLine.TextRuns.Cast().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray());
+ var expected = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+
+ Assert.Equal(expected, actual);
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void Should_Layout_Empty_String()
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(
+ string.Empty,
+ Typeface.Default,
+ 12,
+ Brushes.Black);
+
+ Assert.True(layout.Size.Height > 0);
+ }
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 5961806c5c..8cb010f42b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
+using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
@@ -69,7 +70,105 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
+
+ [Fact]
+ public void Should_Get_Next_Caret_CharacterHit_Bidi()
+ {
+ const string text = "אבג 1 ABC";
+
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ var clusters = new List();
+
+ foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+ {
+ var shapedRun = (ShapedTextCharacters)textRun;
+ clusters.AddRange(shapedRun.IsReversed ?
+ shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
+ shapedRun.ShapedBuffer.GlyphClusters);
+ }
+
+ var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
+
+ foreach (var cluster in clusters)
+ {
+ Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
+
+ nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
+ }
+
+ var lastCharacterHit = nextCharacterHit;
+
+ nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit);
+
+ Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
+
+ Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Previous_Caret_CharacterHit_Bidi()
+ {
+ const string text = "אבג 1 ABC";
+
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ var clusters = new List();
+
+ foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+ {
+ var shapedRun = (ShapedTextCharacters)textRun;
+
+ clusters.AddRange(shapedRun.IsReversed ?
+ shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
+ shapedRun.ShapedBuffer.GlyphClusters);
+ }
+
+ clusters.Reverse();
+
+ var nextCharacterHit = new CharacterHit(text.Length - 1);
+
+ foreach (var cluster in clusters)
+ {
+ var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
+
+ Assert.Equal(cluster, currentCaretIndex);
+
+ nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
+ }
+
+ var lastCharacterHit = nextCharacterHit;
+
+ nextCharacterHit = textLine.GetPreviousCaretCharacterHit(lastCharacterHit);
+
+ Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
+
+ Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
+ }
+ }
+
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
@@ -88,7 +187,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
- var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters)
+ var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters)
.ToArray();
var nextCharacterHit = new CharacterHit(0);
@@ -145,7 +244,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
- var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters)
+ var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters)
.ToArray();
var previousCharacterHit = new CharacterHit(text.Length);
@@ -193,7 +292,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var textSource = new MultiBufferTextSource(defaultProperties);
+ var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
var formatter = new TextFormatterImpl();
@@ -209,13 +308,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var glyphRun = textRun.GlyphRun;
- for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
+ for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++)
{
var cluster = glyphRun.GlyphClusters[i];
- var glyph = glyphRun.GlyphIndices[i];
-
- var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
+ var advance = glyphRun.GlyphAdvances[i];
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
@@ -225,19 +322,20 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- Assert.Equal(currentDistance,
- textLine.GetDistanceFromCharacterHit(new CharacterHit(MultiBufferTextSource.TextRange.Length)));
+ Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
}
}
- [Fact]
- public void Should_Get_CharacterHit_From_Distance()
+ [InlineData("ABC012345")] //LeftToRight
+ [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")] //RightToLeft
+ [Theory]
+ public void Should_Get_CharacterHit_From_Distance(string text)
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var textSource = new MultiBufferTextSource(defaultProperties);
+ var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
@@ -245,35 +343,20 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
- var currentDistance = 0.0;
+ var isRightToLeft = IsRightToLeft(textLine);
+ var rects = BuildRects(textLine);
+ var glyphClusters = BuildGlyphClusters(textLine);
- CharacterHit characterHit;
-
- foreach (var run in textLine.TextRuns)
+ for (var i = 0; i < rects.Count; i++)
{
- var textRun = (ShapedTextCharacters)run;
-
- var glyphRun = textRun.GlyphRun;
-
- for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
- {
- var cluster = glyphRun.GlyphClusters[i];
+ var cluster = glyphClusters[i];
+ var rect = rects[i];
- var glyph = glyphRun.GlyphIndices[i];
+ var characterHit = textLine.GetCharacterHitFromDistance(rect.Left);
- var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
-
- characterHit = textLine.GetCharacterHitFromDistance(currentDistance);
-
- Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
-
- currentDistance += advance;
- }
+ Assert.Equal(isRightToLeft ? cluster + 1 : cluster,
+ characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
-
- characterHit = textLine.GetCharacterHitFromDistance(textLine.Width);
-
- Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex);
}
}
@@ -335,15 +418,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [Fact(Skip = "Verify this")]
- public void Should_Ignore_NewLine_Characters()
+ [Fact]
+ public void TextLineBreak_Should_Contain_TextEndOfLine()
{
using (Start())
{
var defaultTextRunProperties =
new GenericTextRunProperties(Typeface.Default);
- const string text = "01234567🎉\n";
+ const string text = "0123456789";
var source = new SingleBufferTextSource(text, defaultTextRunProperties);
@@ -353,32 +436,88 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
- var nextCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(8, 2));
+ Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ }
+ }
+
+ private static bool IsRightToLeft(TextLine textLine)
+ {
+ return textLine.TextRuns.Cast().Any(x => !x.ShapedBuffer.IsLeftToRight);
+ }
+
+ private static List BuildGlyphClusters(TextLine textLine)
+ {
+ var glyphClusters = new List();
+
+ var shapedTextRuns = textLine.TextRuns.Cast().ToList();
+
+ var lastCluster = -1;
+
+ foreach (var textRun in shapedTextRuns)
+ {
+ var shapedBuffer = textRun.ShapedBuffer;
- Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
+ var currentClusters = shapedBuffer.GlyphClusters.ToList();
+
+ foreach (var currentCluster in currentClusters)
+ {
+ if (lastCluster == currentCluster)
+ {
+ continue;
+ }
+
+ glyphClusters.Add(currentCluster);
+
+ lastCluster = currentCluster;
+ }
}
+
+ return glyphClusters;
}
- [Fact]
- public void TextLineBreak_Should_Contain_TextEndOfLine()
+ private static List BuildRects(TextLine textLine)
{
- using (Start())
- {
- var defaultTextRunProperties =
- new GenericTextRunProperties(Typeface.Default);
+ var rects = new List();
+ var height = textLine.Height;
- const string text = "0123456789";
+ var currentX = 0d;
- var source = new SingleBufferTextSource(text, defaultTextRunProperties);
+ var lastCluster = -1;
- var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
+ var shapedTextRuns = textLine.TextRuns.Cast().ToList();
- var formatter = TextFormatter.Current;
+ foreach (var textRun in shapedTextRuns)
+ {
+ var shapedBuffer = textRun.ShapedBuffer;
+
+ for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
+ {
+ var currentCluster = shapedBuffer.GlyphClusters[index];
+
+ var advance = shapedBuffer.GlyphAdvances[index];
- var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
+ if (lastCluster != currentCluster)
+ {
+ rects.Add(new Rect(currentX, 0, advance, height));
+ }
+ else
+ {
+ var rect = rects[index - 1];
- Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ rects.Remove(rect);
+
+ rect = rect.WithWidth(rect.Width + advance);
+
+ rects.Add(rect);
+ }
+
+ currentX += advance;
+
+ lastCluster = currentCluster;
+ }
}
+
+ return rects;
}
private static IDisposable Start()
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
index 62d2c54ffe..57676ad581 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
@@ -16,17 +16,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var text = "\n\r\n".AsMemory();
- var glyphRun = TextShaper.Current.ShapeText(
+ var shapedBuffer = TextShaper.Current.ShapeText(
text,
- Typeface.Default,
+ Typeface.Default.GlyphTypeface,
12,
- CultureInfo.CurrentCulture);
+ CultureInfo.CurrentCulture, 0);
- Assert.Equal(glyphRun.Characters.Length, text.Length);
- Assert.Equal(glyphRun.GlyphClusters.Length, text.Length);
- Assert.Equal(0, glyphRun.GlyphClusters[0]);
- Assert.Equal(1, glyphRun.GlyphClusters[1]);
- Assert.Equal(1, glyphRun.GlyphClusters[2]);
+ Assert.Equal(shapedBuffer.Text.Length, text.Length);
+ Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length);
+ Assert.Equal(0, shapedBuffer.GlyphClusters[0]);
+ Assert.Equal(1, shapedBuffer.GlyphClusters[1]);
+ Assert.Equal(1, shapedBuffer.GlyphClusters[2]);
}
}
diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
index d4abf9416a..c3cf6cd8d3 100644
--- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
+++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
@@ -8,11 +8,13 @@
latest
..\..\build\avalonia.snk
false
+ true
+
@@ -25,8 +27,8 @@
-
+
diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
new file mode 100644
index 0000000000..002da66070
--- /dev/null
+++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class HarfBuzzFontManagerImpl : IFontManagerImpl
+ {
+ private readonly Typeface[] _customTypefaces;
+ private readonly string _defaultFamilyName;
+
+ private static readonly Typeface _defaultTypeface =
+ new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono");
+ private static readonly Typeface _italicTypeface =
+ new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans");
+ private static readonly Typeface _emojiTypeface =
+ new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji");
+
+ public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")
+ {
+ _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
+ _defaultFamilyName = defaultFamilyName;
+ }
+
+ public string GetDefaultFontFamilyName()
+ {
+ return _defaultFamilyName;
+ }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ return _customTypefaces.Select(x => x.FontFamily!.Name);
+ }
+
+ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
+ CultureInfo culture, out Typeface fontKey)
+ {
+ foreach (var customTypeface in _customTypefaces)
+ {
+ var glyphTypeface = customTypeface.GlyphTypeface;
+
+ if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+ {
+ continue;
+ }
+
+ fontKey = customTypeface;
+
+ return true;
+ }
+
+ fontKey = default;
+
+ return false;
+ }
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ var fontFamily = typeface.FontFamily;
+
+ if (fontFamily == null)
+ {
+ return null;
+ }
+
+ if (fontFamily.IsDefault)
+ {
+ fontFamily = _defaultTypeface.FontFamily;
+ }
+
+ if (fontFamily!.Key == null)
+ {
+ return null;
+ }
+
+ var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
+
+ var asset = fontAssets.First();
+
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ if (assetLoader == null)
+ {
+ throw new NotSupportedException("IAssetLoader is not registered.");
+ }
+
+ var stream = assetLoader.Open(asset);
+
+ return new HarfBuzzGlyphTypefaceImpl(stream);
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..32e0434cd4
--- /dev/null
+++ b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
@@ -0,0 +1,158 @@
+using System;
+using System.IO;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+
+namespace Avalonia.UnitTests
+{
+ public class HarfBuzzGlyphTypefaceImpl : IGlyphTypefaceImpl
+ {
+ private bool _isDisposed;
+ private Blob _blob;
+
+ public HarfBuzzGlyphTypefaceImpl(Stream data, bool isFakeBold = false, bool isFakeItalic = false)
+ {
+ _blob = Blob.FromStream(data);
+
+ Face = new Face(_blob, 0);
+
+ Font = new Font(Face);
+
+ Font.SetFunctionsOpenType();
+
+ Font.GetScale(out var scale, out _);
+
+ DesignEmHeight = (short)scale;
+
+ var metrics = Font.OpenTypeMetrics;
+
+ const double defaultFontRenderingEmSize = 12.0;
+
+ Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * DesignEmHeight);
+
+ IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b'));
+
+ IsFakeBold = isFakeBold;
+
+ IsFakeItalic = isFakeItalic;
+ }
+
+ public Face Face { get; }
+
+ public Font Font { get; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public bool IsFixedPitch { get; }
+
+ public bool IsFakeBold { get; }
+
+ public bool IsFakeItalic { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ var glyphs = new ushort[codepoints.Length];
+
+ for (var i = 0; i < codepoints.Length; i++)
+ {
+ if (Font.TryGetGlyph(codepoints[i], out var glyph))
+ {
+ glyphs[i] = (ushort)glyph;
+ }
+ }
+
+ return glyphs;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var glyphIndices = new uint[glyphs.Length];
+
+ for (var i = 0; i < glyphs.Length; i++)
+ {
+ glyphIndices[i] = glyphs[i];
+ }
+
+ return Font.GetHorizontalGlyphAdvances(glyphIndices);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ Font?.Dispose();
+ Face?.Dispose();
+ _blob?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
new file mode 100644
index 0000000000..8ad3284490
--- /dev/null
+++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
+using Avalonia.Utilities;
+using HarfBuzzSharp;
+using Buffer = HarfBuzzSharp.Buffer;
+
+namespace Avalonia.UnitTests
+{
+ public class HarfBuzzTextShaperImpl : ITextShaperImpl
+ {
+ public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
+ CultureInfo culture, sbyte bidiLevel)
+ {
+ using (var buffer = new Buffer())
+ {
+ buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+
+ MergeBreakPair(buffer);
+
+ buffer.GuessSegmentProperties();
+
+ buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
+
+ buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+
+ var font = ((HarfBuzzGlyphTypefaceImpl)typeface.PlatformImpl).Font;
+
+ font.Shape(buffer);
+
+ if (buffer.Direction == Direction.RightToLeft)
+ {
+ buffer.Reverse();
+ }
+
+ font.GetScale(out var scaleX, out _);
+
+ var textScale = fontRenderingEmSize / scaleX;
+
+ var bufferLength = buffer.Length;
+
+ var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+
+ var glyphInfos = buffer.GetGlyphInfoSpan();
+
+ var glyphPositions = buffer.GetGlyphPositionSpan();
+
+ for (var i = 0; i < bufferLength; i++)
+ {
+ var sourceInfo = glyphInfos[i];
+
+ var glyphIndex = (ushort)sourceInfo.Codepoint;
+
+ var glyphCluster = (int)sourceInfo.Cluster;
+
+ var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
+
+ var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+
+ var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
+
+ shapedBuffer[i] = targetInfo;
+ }
+
+ return shapedBuffer;
+ }
+ }
+
+ private static void MergeBreakPair(Buffer buffer)
+ {
+ var length = buffer.Length;
+
+ var glyphInfos = buffer.GetGlyphInfoSpan();
+
+ var second = glyphInfos[length - 1];
+
+ if (!new Codepoint((int)second.Codepoint).IsBreakChar)
+ {
+ return;
+ }
+
+ if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
+ {
+ var first = glyphInfos[length - 2];
+
+ first.Codepoint = '\u200C';
+ second.Codepoint = '\u200C';
+ second.Cluster = first.Cluster;
+
+ unsafe
+ {
+ fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 2])
+ {
+ *p = first;
+ }
+
+ fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1])
+ {
+ *p = second;
+ }
+ }
+ }
+ else
+ {
+ second.Codepoint = '\u200C';
+
+ unsafe
+ {
+ fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1])
+ {
+ *p = second;
+ }
+ }
+ }
+ }
+
+ private static Vector GetGlyphOffset(ReadOnlySpan glyphPositions, int index, double textScale)
+ {
+ var position = glyphPositions[index];
+
+ var offsetX = position.XOffset * textScale;
+
+ var offsetY = position.YOffset * textScale;
+
+ return new Vector(offsetX, offsetY);
+ }
+
+ private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale)
+ {
+ // Depends on direction of layout
+ // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
+ return glyphPositions[index].XAdvance * textScale;
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 1f632034be..2858a9f228 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -10,18 +10,6 @@ namespace Avalonia.UnitTests
{
public class MockPlatformRenderInterface : IPlatformRenderInterface
{
- public IFormattedTextImpl CreateFormattedText(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList spans)
- {
- return Mock.Of();
- }
-
public IGeometryImpl CreateEllipseGeometry(Rect rect)
{
return Mock.Of();
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index fe1c34385f..3018c07819 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -1,6 +1,6 @@
-using System;
-using System.Globalization;
+using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@@ -9,29 +9,24 @@ namespace Avalonia.UnitTests
{
public class MockTextShaperImpl : ITextShaperImpl
{
- public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
+ CultureInfo culture, sbyte bidiLevel)
{
- var glyphTypeface = typeface.GlyphTypeface;
- var glyphIndices = new ushort[text.Length];
- var glyphCount = 0;
+ var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
- for (var i = 0; i < text.Length;)
+ for (var i = 0; i < shapedBuffer.Length;)
{
- var index = i;
-
+ var glyphCluster = i + text.Start;
var codepoint = Codepoint.ReadAt(text, i, out var count);
- i += count;
-
- var glyph = glyphTypeface.GetGlyph(codepoint);
+ var glyphIndex = typeface.GetGlyph(codepoint);
- glyphIndices[index] = glyph;
+ shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster);
- glyphCount++;
+ i += count;
}
- return new GlyphRun(glyphTypeface, fontRenderingEmSize,
- new ReadOnlySlice(glyphIndices.AsMemory(0, glyphCount)), characters: text);
+ return shapedBuffer;
}
}
}
diff --git a/tests/Avalonia.UnitTests/RuntimeInfo.cs b/tests/Avalonia.UnitTests/RuntimeInfo.cs
deleted file mode 100644
index eb5781a725..0000000000
--- a/tests/Avalonia.UnitTests/RuntimeInfo.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Avalonia.Platform;
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Text;
-
-namespace Avalonia.Shared.PlatformSupport
-{
- internal partial class StandardRuntimePlatform : IRuntimePlatform
- {
- public RuntimePlatformInfo GetRuntimeInfo()
- {
- return new RuntimePlatformInfo();
- }
- }
-}
diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs
index 8d27562146..2c4ae06fb2 100644
--- a/tests/Avalonia.UnitTests/TestServices.cs
+++ b/tests/Avalonia.UnitTests/TestServices.cs
@@ -5,7 +5,7 @@ using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
-using Avalonia.Shared.PlatformSupport;
+using Avalonia.PlatformSupport;
using Avalonia.Styling;
using Avalonia.Themes.Default;
using Avalonia.Rendering;
@@ -58,6 +58,12 @@ namespace Avalonia.UnitTests
public static readonly TestServices RealStyler = new TestServices(
styler: new Styler());
+ public static readonly TestServices TextServices = new TestServices(
+ assetLoader: new AssetLoader(),
+ renderInterface: new MockPlatformRenderInterface(),
+ fontManagerImpl: new HarfBuzzFontManagerImpl(),
+ textShaperImpl: new HarfBuzzTextShaperImpl());
+
public TestServices(
IAssetLoader assetLoader = null,
IFocusManager focusManager = null,
@@ -168,7 +174,7 @@ namespace Avalonia.UnitTests
};
var baseLight = (IStyle)AvaloniaXamlLoader.Load(
- new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"));
+ new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"));
result.Add(baseLight);
return result;
@@ -177,14 +183,6 @@ namespace Avalonia.UnitTests
private static IPlatformRenderInterface CreateRenderInterfaceMock()
{
return Mock.Of(x =>
- x.CreateFormattedText(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()) == Mock.Of() &&
x.CreateStreamGeometry() == Mock.Of(
y => y.Open() == Mock.Of()));
}
diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs
index 2d37057779..bb6e53d74f 100644
--- a/tests/Avalonia.UnitTests/UnitTestApplication.cs
+++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs
@@ -10,6 +10,7 @@ using System.Reactive.Disposables;
using System.Reactive.Concurrency;
using Avalonia.Input.Platform;
using Avalonia.Animation;
+using Avalonia.PlatformSupport;
namespace Avalonia.UnitTests
{
@@ -29,6 +30,11 @@ namespace Avalonia.UnitTests
RegisterServices();
}
+ static UnitTestApplication()
+ {
+ AssetLoader.RegisterResUriParsers();
+ }
+
public static new UnitTestApplication Current => (UnitTestApplication)Application.Current;
public TestServices Services => _services;
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
index 6e5b8eb637..a48639a426 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
@@ -48,7 +48,16 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Should_Use_FontManagerOptions_FontFallback()
{
- var options = new FontManagerOptions { FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default} } };
+ var options = new FontManagerOptions
+ {
+ FontFallbacks = new[]
+ {
+ new FontFallback
+ {
+ FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default
+ }
+ }
+ };
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(fontManagerImpl: new MockFontManagerImpl())))
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
index 58feb4714a..ba334fdb74 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System.Linq;
+using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Utilities;
@@ -14,14 +15,15 @@ namespace Avalonia.Visuals.UnitTests.Media
.Bind().ToSingleton();
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 3, 30)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 1, 0, 10)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 0, 20)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 1, 30)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 3, 30)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 1, 0, 10)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 2, 0, 20)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 2, 1, 30)]
[Theory]
- public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
+ public void Should_Get_Distance_From_CharacterHit(double[] advances, int[] clusters, int start, int trailingLength, double expectedDistance)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var characterHit = new CharacterHit(start, trailingLength);
@@ -32,14 +34,15 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 1, 1, true)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 26.0, 0, 3, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 20.0, 1, 1, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 26.0, 2, 1, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 35.0, 2, 1, false)]
[Theory]
- public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
+ public void Should_Get_CharacterHit_FromDistance(double[] advances, int[] clusters, double distance, int start,
int trailingLengthExpected, bool isInsideExpected)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
@@ -52,17 +55,18 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 10, 11, 12 }, 0, -1, 10, 1, 10)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 10, 11, 12 }, 0, 15, 12, 1, 10)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 10, 11, 12 }, 0, -1, 10, 1, 10)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 10, 11, 12 }, 0, 15, 12, 1, 10)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
+ [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
+ [InlineData(new double[] { 10, 20, 0, 10 }, new int[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 20, 0, 10 }, new int[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 0, 20, 10 }, new int[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
[Theory]
- public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
+ public void Should_Find_Nearest_CharacterHit(double[] advances, int[] clusters, int bidiLevel,
int index, int expectedIndex, int expectedLength, double expectedWidth)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
@@ -75,21 +79,22 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
+ [InlineData(new double[] { 0, 0, 30 }, new int[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
+ [InlineData(new double[] { 30, 0, 0, 10 }, new int[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
+ [InlineData(new double[] { 10, 0, 0, 30 }, new int[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
+ [InlineData(new double[] { 10, 30, 0, 0, 10 }, new int[] { 0, 1, 1, 1, 4 }, 1, 0, 4, 0, 0)]
+ [InlineData(new double[] { 10, 0, 0, 30, 10 }, new int[] { 4, 1, 1, 1, 0 }, 1, 0, 4, 0, 1)]
[Theory]
- public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
- int currentIndex, int currentLength,
+ public void Should_Get_Next_CharacterHit(double[] advances,int[] clusters,
+ int firstCharacterIndex, int trailingLength,
int nextIndex, int nextLength,
int bidiLevel)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
- var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+ var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(firstCharacterIndex, trailingLength));
Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
@@ -97,18 +102,19 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
+ [InlineData(new double[] { 0, 0, 30 }, new int[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
+ [InlineData(new double[] { 30, 0, 0, 10 }, new int[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
+ [InlineData(new double[] { 0, 0, 30, 10 }, new int[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
+ [InlineData(new double[] { 10, 30, 0, 0, 10 }, new int[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
+ [InlineData(new double[] { 10, 0, 0, 30, 10 }, new int[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
[Theory]
- public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
+ public void Should_Get_Previous_CharacterHit(double[] advances, int[] clusters,
int currentIndex, int currentLength,
int previousIndex, int previousLength,
int bidiLevel)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
@@ -119,15 +125,16 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0)]
- [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 1)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 0)]
- [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 1)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 0)]
- [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 1)]
+ [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0)]
+ [InlineData(new double[] { 0, 0, 30 }, new int[] { 0, 0, 0 }, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new int[] { 0, 0, 0, 3 }, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new int[] { 3, 0, 0, 0 }, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new int[] { 0, 1, 1, 1, 4 }, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new int[] { 4, 1, 1, 1, 0 }, 1)]
[Theory]
- public void Should_Find_Glyph_Index(double[] advances, ushort[] clusters, int bidiLevel)
+ public void Should_Find_Glyph_Index(double[] advances, int[] clusters, int bidiLevel)
{
+ using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
if (glyphRun.IsLeftToRight)
@@ -169,17 +176,17 @@ namespace Avalonia.Visuals.UnitTests.Media
}
}
- private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
+ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0)
{
var count = glyphAdvances.Length;
var glyphIndices = new ushort[count];
- var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[glyphClusters.Length - 1];
+ var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1];
- var characters = new ReadOnlySlice(new char[count], start, count);
+ var characters = new ReadOnlySlice(Enumerable.Repeat('a', count).ToArray(), start, count);
- return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
- glyphClusters: glyphClusters, characters: characters, biDiLevel: bidiLevel);
+ return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, characters, glyphIndices, glyphAdvances,
+ glyphClusters: glyphClusters, biDiLevel: bidiLevel);
}
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs
new file mode 100644
index 0000000000..f8a2abc716
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs
@@ -0,0 +1,85 @@
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Utilities;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
+{
+ public class BiDiAlgorithmTests
+ {
+ private readonly ITestOutputHelper _outputHelper;
+
+ public BiDiAlgorithmTests(ITestOutputHelper outputHelper)
+ {
+ _outputHelper = outputHelper;
+ }
+
+ [Fact(Skip = "Only run when the Unicode spec changes.")]
+ public void Should_Process()
+ {
+ var generator = new BiDiTestDataGenerator();
+
+ foreach(var testData in generator)
+ {
+ Assert.True(Run(testData));
+ }
+ }
+
+ private bool Run(BiDiTestData testData)
+ {
+ var bidi = BidiAlgorithm.Instance.Value;
+
+ // Run the algorithm...
+ ArraySlice resultLevels;
+
+ bidi.Process(
+ testData.Classes,
+ ArraySlice.Empty,
+ ArraySlice.Empty,
+ testData.ParagraphEmbeddingLevel,
+ false,
+ null,
+ null,
+ null);
+
+ resultLevels = bidi.ResolvedLevels;
+
+ // Check the results match
+ var pass = true;
+
+ if (resultLevels.Length == testData.Levels.Length)
+ {
+ for (var i = 0; i < testData.Levels.Length; i++)
+ {
+ if (testData.Levels[i] == -1)
+ {
+ continue;
+ }
+
+ if (resultLevels[i] != testData.Levels[i])
+ {
+ pass = false;
+ break;
+ }
+ }
+ }
+ else
+ {
+ pass = false;
+ }
+
+ if (!pass)
+ {
+ _outputHelper.WriteLine($"Failed line {testData.LineNumber}");
+ _outputHelper.WriteLine($" Data: {string.Join(" ", testData.Classes)}");
+ _outputHelper.WriteLine($" Embed Level: {testData.ParagraphEmbeddingLevel}");
+ _outputHelper.WriteLine($" Expected: {string.Join(" ", testData.Levels)}");
+ _outputHelper.WriteLine($" Actual: {string.Join(" ", resultLevels)}");
+
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTestDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTestDataGenerator.cs
new file mode 100644
index 0000000000..2f1064be21
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTestDataGenerator.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+
+namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
+{
+ internal class BiDiClassTestDataGenerator : IEnumerable
+ {
+ private readonly List _testData;
+
+ public BiDiClassTestDataGenerator()
+ {
+ _testData = ReadData();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _testData.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private static List ReadData()
+ {
+ var testData = new List();
+
+ using (var client = new HttpClient())
+ {
+ var url = Path.Combine(UnicodeDataGenerator.Ucd, "BidiCharacterTest.txt");
+
+ using (var result = client.GetAsync(url).GetAwaiter().GetResult())
+ {
+ if (!result.IsSuccessStatusCode)
+ return testData;
+
+ using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
+ using (var reader = new StreamReader(stream))
+ {
+ var lineNumber = 0;
+
+ // Process each line
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+
+ lineNumber++;
+
+ if (line == null)
+ {
+ break;
+ }
+
+ if (line.StartsWith("#") || string.IsNullOrEmpty(line))
+ {
+ continue;
+ }
+
+ // Split into fields
+ var fields = line.Split(';');
+
+ // Parse field 0 - code points
+ var codePoints = fields[0].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => Convert.ToInt32(x, 16)).ToArray();
+
+ // Parse field 1 - paragraph level
+ var paragraphLevel = sbyte.Parse(fields[1]);
+
+ // Parse field 2 - resolved paragraph level
+ var resolvedParagraphLevel = sbyte.Parse(fields[2]);
+
+ // Parse field 3 - resolved levels
+ var resolvedLevels = fields[3].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => x == "x" ? (sbyte)-1 : Convert.ToSByte(x)).ToArray();
+
+ // Parse field 4 - resolved levels
+ var resolvedOrder = fields[4].Split(' ').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).Select(x => Convert.ToInt32(x)).ToArray();
+
+ testData.Add(new BiDiClassData
+ {
+ LineNumber = lineNumber,
+ CodePoints = codePoints,
+ ParagraphLevel = paragraphLevel,
+ ResolvedParagraphLevel = resolvedParagraphLevel,
+ ResolvedLevels = resolvedLevels,
+ ResolvedOrder = resolvedOrder
+ });
+ }
+ }
+ }
+ }
+
+ return testData;
+ }
+
+
+ }
+
+ internal struct BiDiClassData
+ {
+ public int LineNumber { get; set; }
+ public int[] CodePoints{ get; set; }
+ public sbyte ParagraphLevel{ get; set; }
+ public sbyte ResolvedParagraphLevel{ get; set; }
+ public sbyte[] ResolvedLevels{ get; set; }
+ public int[] ResolvedOrder{ get; set; }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTests.cs
new file mode 100644
index 0000000000..1ed33e6132
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiClassTests.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Media.TextFormatting.Unicode;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
+{
+ public class BiDiClassTests
+ {
+ private readonly ITestOutputHelper _outputHelper;
+
+ public BiDiClassTests(ITestOutputHelper outputHelper)
+ {
+ _outputHelper = outputHelper;
+ }
+
+ [Fact(Skip = "Only run when the Unicode spec changes.")]
+ public void Should_Resolve()
+ {
+ var generator = new BiDiClassTestDataGenerator();
+
+ foreach (var testData in generator)
+ {
+ Assert.True(Run(testData));
+ }
+ }
+
+ private bool Run(BiDiClassData t)
+ {
+ var bidi = BidiAlgorithm.Instance.Value;
+ var bidiData = new BidiData(t.ParagraphLevel);
+
+ var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray());
+
+ // Append
+ bidiData.Append(text.AsMemory());
+
+ // Act
+ bidi.Process(bidiData);
+
+ var resultLevels = bidi.ResolvedLevels;
+ var resultParagraphLevel = bidi.ResolvedParagraphEmbeddingLevel;
+
+ // Assert
+ var passed = true;
+
+ if (t.ResolvedParagraphLevel != resultParagraphLevel)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < t.ResolvedLevels.Length; i++)
+ {
+ if (t.ResolvedLevels[i] == -1)
+ {
+ continue;
+ }
+
+ if (t.ResolvedLevels[i] != resultLevels[i])
+ {
+ passed = false;
+ break;
+ }
+ }
+
+ if (passed)
+ {
+ return true;
+ }
+
+ _outputHelper.WriteLine($"Failed line {t.LineNumber}");
+
+ _outputHelper.WriteLine(
+ $" Code Points: {string.Join(" ", t.CodePoints.Select(x => x.ToString("X4")))}");
+
+ _outputHelper.WriteLine(
+ $" Pair Bracket Types: {string.Join(" ", bidiData.PairedBracketTypes.Select(x => " " + x.ToString()))}");
+
+ _outputHelper.WriteLine(
+ $" Pair Bracket Values: {string.Join(" ", bidiData.PairedBracketValues.Select(x => x.ToString("X4")))}");
+ _outputHelper.WriteLine($" Embed Level: {t.ParagraphLevel}");
+ _outputHelper.WriteLine($" Expected Embed Level: {t.ResolvedParagraphLevel}");
+ _outputHelper.WriteLine($" Actual Embed Level: {resultParagraphLevel}");
+ _outputHelper.WriteLine($" Directionality: {string.Join(" ", bidiData.Classes)}");
+ _outputHelper.WriteLine($" Expected Levels: {string.Join(" ", t.ResolvedLevels)}");
+ _outputHelper.WriteLine($" Actual Levels: {string.Join(" ", resultLevels)}");
+
+ return false;
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiTestDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiTestDataGenerator.cs
new file mode 100644
index 0000000000..4102de10ec
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiTestDataGenerator.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using Avalonia.Media.TextFormatting.Unicode;
+
+namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
+{
+ internal class BiDiTestDataGenerator : IEnumerable
+ {
+ private readonly List _testData;
+
+ public BiDiTestDataGenerator()
+ {
+ _testData = ReadTestData();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _testData.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private static List ReadTestData()
+ {
+ var testData = new List();
+
+ using (var client = new HttpClient())
+ {
+ var url = Path.Combine(UnicodeDataGenerator.Ucd, "BidiTest.txt");
+
+ using (var result = client.GetAsync(url).GetAwaiter().GetResult())
+ {
+ if (!result.IsSuccessStatusCode)
+ return testData;
+
+ using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
+ using (var reader = new StreamReader(stream))
+ {
+ var lineNumber = 0;
+
+ // Process each line
+ int[] levels = null;
+
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+
+ lineNumber++;
+
+ if (line == null)
+ {
+ break;
+ }
+
+ if (line.StartsWith("#") || string.IsNullOrEmpty(line))
+ {
+ continue;
+ }
+
+ // Directive?
+ if (line.StartsWith("@"))
+ {
+ if (line.StartsWith("@Levels:"))
+ {
+ levels = line.Substring(8).Trim().Split(' ').Where(x => x.Length > 0).Select(x =>
+ {
+ if (x == "x")
+ {
+ return -1;
+ }
+
+ return int.Parse(x);
+
+ }).ToArray();
+ }
+
+ continue;
+ }
+
+ // Split data line
+ var parts = line.Split(';');
+
+ // Get the directions
+ var directions = parts[0].Split(' ').Select(PropertyValueAliasHelper.GetBiDiClass)
+ .ToArray();
+
+ // Get the bit set
+ var bitset = Convert.ToInt32(parts[1].Trim(), 16);
+
+ for (var bit = 1; bit < 8; bit <<= 1)
+ {
+ if ((bitset & bit) == 0)
+ {
+ continue;
+ }
+
+ sbyte paragraphEmbeddingLevel;
+
+ switch (bit)
+ {
+ case 1:
+ paragraphEmbeddingLevel = 2; // Auto
+ break;
+
+ case 2:
+ paragraphEmbeddingLevel = 0; // LTR
+ break;
+
+ case 4:
+ paragraphEmbeddingLevel = 1; // RTL
+ break;
+
+ default:
+ throw new NotSupportedException();
+ }
+
+ testData.Add(new BiDiTestData
+ {
+ LineNumber = lineNumber,
+ Classes = directions,
+ ParagraphEmbeddingLevel = paragraphEmbeddingLevel,
+ Levels = levels
+ });
+ }
+ }
+ }
+ }
+ }
+
+ return testData;
+ }
+ }
+
+ internal class BiDiTestData
+ {
+ public int LineNumber { get; set; }
+ public BidiClass[] Classes { get; set; }
+ public sbyte ParagraphEmbeddingLevel { get; set; }
+ public int[] Levels { get; set; }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs
index cbe8edefb6..da9e494405 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Net.Http;
using System.Text.RegularExpressions;
using Avalonia.Media.TextFormatting.Unicode;
+using Xunit;
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{
@@ -11,93 +12,145 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{
public const string Ucd = "https://www.unicode.org/Public/13.0.0/ucd/";
- public static void Execute()
+ public static UnicodeTrie GenerateBiDiTrie(out BiDiDataEntries biDiDataEntries,out Dictionary biDiData)
{
- var codepoints = new Dictionary();
+ biDiData = new Dictionary();
- var generalCategoryEntries =
- UnicodeEnumsGenerator.CreateGeneralCategoryEnum();
+ var biDiClassEntries =
+ UnicodeEnumsGenerator.CreateBiDiClassEnum();
- var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryEntries);
+ var biDiClassMappings = CreateTagToIndexMappings(biDiClassEntries);
- var generalCategoryData = ReadGeneralCategoryData();
+ var biDiClassData = ReadBiDiData();
- foreach (var (range, name) in generalCategoryData)
+ foreach (var (range, name) in biDiClassData)
{
- var generalCategory = generalCategoryMappings[name];
+ var biDiClass = biDiClassMappings[name];
- AddGeneralCategoryRange(codepoints, range, generalCategory);
+ AddBiDiClassRange(biDiData, range, biDiClass);
}
- var scriptEntries = UnicodeEnumsGenerator.CreateScriptEnum();
+ var biDiPairedBracketTypeEntries = UnicodeEnumsGenerator.CreateBiDiPairedBracketTypeEnum();
- var scriptMappings = CreateNameToIndexMappings(scriptEntries);
+ var biDiPairedBracketTypeMappings = CreateTagToIndexMappings(biDiPairedBracketTypeEntries);
- var scriptData = ReadScriptData();
+ var biDiPairedBracketData = ReadBiDiPairedBracketData();
+
+ foreach (var (range, name) in biDiPairedBracketData)
+ {
+ var bracketType = biDiPairedBracketTypeMappings[name];
- foreach (var (range, name) in scriptData)
+ AddBiDiBracket(biDiData, range, bracketType);
+ }
+
+ var biDiTrieBuilder = new UnicodeTrieBuilder(/*initialValue*/);
+
+ foreach (var properties in biDiData.Values)
{
- var script = scriptMappings[name];
+ //[bracket]|[bracketType]|[biDiClass]
+ var value = (properties.BiDiClass << UnicodeData.BIDICLASS_SHIFT) |
+ (properties.BracketType << UnicodeData.BIDIPAIREDBRACKEDTYPE_SHIFT) | properties.Bracket;
- AddScriptRange(codepoints, range, script);
+ biDiTrieBuilder.Set(properties.Codepoint, (uint)value);
}
- var biDiClassEntries =
- UnicodeEnumsGenerator.CreateBiDiClassEnum();
+ biDiDataEntries = new BiDiDataEntries()
+ {
+ PairedBracketTypes = biDiPairedBracketTypeEntries, BiDiClasses = biDiClassEntries
+ };
+
+ using (var stream = File.Create("Generated\\BiDi.trie"))
+ {
+ var trie = biDiTrieBuilder.Freeze();
- var biDiClassMappings = CreateTagToIndexMappings(biDiClassEntries);
+ trie.Save(stream);
- var biDiData = ReadBiDiData();
+ return trie;
+ }
+ }
- foreach (var (range, name) in biDiData)
- {
- var biDiClass = biDiClassMappings[name];
+ public static UnicodeTrie GenerateUnicodeDataTrie(out UnicodeDataEntries dataEntries, out Dictionary unicodeData)
+ {
+ var generalCategoryEntries =
+ UnicodeEnumsGenerator.CreateGeneralCategoryEnum();
- AddBiDiClassRange(codepoints, range, biDiClass);
- }
+ var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryEntries);
+
+ var scriptEntries = UnicodeEnumsGenerator.CreateScriptEnum();
+ var scriptMappings = CreateNameToIndexMappings(scriptEntries);
+
var lineBreakClassEntries =
UnicodeEnumsGenerator.CreateLineBreakClassEnum();
var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassEntries);
- var lineBreakClassData = ReadLineBreakClassData();
+ unicodeData = GetUnicodeData(generalCategoryMappings, scriptMappings, lineBreakClassMappings);
+
+ var unicodeDataTrieBuilder = new UnicodeTrieBuilder(/*initialValue*/);
+
+ foreach (var properties in unicodeData.Values)
+ {
+ //[line break]|[biDi]|[script]|[category]
+ var value = (properties.LineBreakClass << UnicodeData.LINEBREAK_SHIFT) |
+ (properties.Script << UnicodeData.SCRIPT_SHIFT) | properties.GeneralCategory;
- foreach (var (range, name) in lineBreakClassData)
+ unicodeDataTrieBuilder.Set(properties.Codepoint, (uint)value);
+ }
+
+ dataEntries = new UnicodeDataEntries
{
- var lineBreakClass = lineBreakClassMappings[name];
+ Scripts = scriptEntries,
+ GeneralCategories = generalCategoryEntries,
+ LineBreakClasses = lineBreakClassEntries
+ };
- AddLineBreakClassRange(codepoints, range, lineBreakClass);
+ using (var stream = File.Create("Generated\\UnicodeData.trie"))
+ {
+ var trie = unicodeDataTrieBuilder.Freeze();
+
+ trie.Save(stream);
+
+ return trie;
}
+ }
- //const int initialValue = (0 << UnicodeData.LINEBREAK_SHIFT) |
- // (0 << UnicodeData.BIDI_SHIFT) |
- // (0 << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other;
+ private static Dictionary GetUnicodeData(IReadOnlyDictionary generalCategoryMappings,
+ IReadOnlyDictionary scriptMappings, IReadOnlyDictionary lineBreakClassMappings)
+ {
+ var unicodeData = new Dictionary();
+
+ var generalCategoryData = ReadGeneralCategoryData();
- var builder = new UnicodeTrieBuilder(/*initialValue*/);
+ foreach (var (range, name) in generalCategoryData)
+ {
+ var generalCategory = generalCategoryMappings[name];
- foreach (var properties in codepoints.Values)
+ AddGeneralCategoryRange(unicodeData, range, generalCategory);
+ }
+
+ var scriptData = ReadScriptData();
+
+ foreach (var (range, name) in scriptData)
{
- //[line break]|[biDi]|[script]|[category]
- var value = (properties.LineBreakClass << UnicodeData.LINEBREAK_SHIFT) |
- (properties.BiDiClass << UnicodeData.BIDI_SHIFT) |
- (properties.Script << UnicodeData.SCRIPT_SHIFT) | properties.GeneralCategory;
+ var script = scriptMappings[name];
- builder.Set(properties.Codepoint, (uint)value);
+ AddScriptRange(unicodeData, range, script);
}
+
+ var lineBreakClassData = ReadLineBreakClassData();
- using (var stream = File.Create("Generated\\UnicodeData.trie"))
+ foreach (var (range, name) in lineBreakClassData)
{
- var trie = builder.Freeze();
+ var lineBreakClass = lineBreakClassMappings[name];
- trie.Save(stream);
+ AddLineBreakClassRange(unicodeData, range, lineBreakClass);
}
- UnicodeEnumsGenerator.CreatePropertyValueAliasHelper(scriptEntries, generalCategoryEntries,
- biDiClassEntries, lineBreakClassEntries);
+ return unicodeData;
}
- private static Dictionary CreateTagToIndexMappings(List entries)
+ private static Dictionary CreateTagToIndexMappings(IReadOnlyList entries)
{
var mappings = new Dictionary();
@@ -109,7 +162,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
return mappings;
}
- private static Dictionary CreateNameToIndexMappings(List entries)
+ private static Dictionary CreateNameToIndexMappings(IReadOnlyList entries)
{
var mappings = new Dictionary();
@@ -153,14 +206,14 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
}
}
- private static void AddBiDiClassRange(Dictionary codepoints, CodepointRange range,
+ private static void AddBiDiClassRange(Dictionary codepoints, CodepointRange range,
int biDiClass)
{
for (var i = range.Start; i <= range.End; i++)
{
if (!codepoints.ContainsKey(i))
{
- codepoints.Add(i, new UnicodeDataItem { Codepoint = i, BiDiClass = biDiClass });
+ codepoints.Add(i, new BiDiDataItem { Codepoint = i, BiDiClass = biDiClass });
}
else
{
@@ -169,6 +222,23 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
}
}
+ private static void AddBiDiBracket(Dictionary codepoints, CodepointRange range,
+ int bracketType)
+ {
+ if (!codepoints.ContainsKey(range.Start))
+ {
+ codepoints.Add(range.Start,
+ new BiDiDataItem { Codepoint = range.Start, Bracket = range.End, BracketType = bracketType });
+ }
+ else
+ {
+ var codepoint = codepoints[range.Start];
+
+ codepoint.Bracket = range.End;
+ codepoint.BracketType = bracketType;
+ }
+ }
+
private static void AddLineBreakClassRange(Dictionary codepoints, CodepointRange range,
int lineBreakClass)
{
@@ -204,12 +274,68 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{
return ReadUnicodeData("extracted/DerivedLineBreak.txt");
}
+
+ public static List<(CodepointRange, string)> ReadBiDiPairedBracketData()
+ {
+ const string file = "BidiBrackets.txt";
+
+ var data = new List<(CodepointRange, string)>();
+
+ var regex = new Regex(@"^([0-9A-F]+);\s([0-9A-F]+);\s([ocn])");
+
+ using (var client = new HttpClient())
+ {
+ var url = Path.Combine(Ucd, file);
+
+ using (var result = client.GetAsync(url).GetAwaiter().GetResult())
+ {
+ if (!result.IsSuccessStatusCode)
+ {
+ return data;
+ }
+
+ using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
+ using (var reader = new StreamReader(stream))
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine();
+
+ if (string.IsNullOrEmpty(line))
+ {
+ continue;
+ }
+
+ var match = regex.Match(line);
+
+ if (!match.Success)
+ {
+ continue;
+ }
+
+ var start = Convert.ToInt32(match.Groups[1].Value, 16);
+
+ var end = start;
+
+ if (!string.IsNullOrEmpty(match.Groups[2].Value))
+ {
+ end = Convert.ToInt32(match.Groups[2].Value, 16);
+ }
+
+ data.Add((new CodepointRange(start, end), match.Groups[3].Value));
+ }
+ }
+ }
+ }
+
+ return data;
+ }
private static List<(CodepointRange, string)> ReadUnicodeData(string file)
{
var data = new List<(CodepointRange, string)>();
- var rx = new Regex(@"([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+(\w+)\s+#.*", RegexOptions.Compiled);
+ var regex = new Regex(@"([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+(\w+)\s+#.*", RegexOptions.Compiled);
using (var client = new HttpClient())
{
@@ -234,7 +360,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
continue;
}
- var match = rx.Match(line);
+ var match = regex.Match(line);
if (!match.Success)
{
@@ -271,6 +397,32 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
public int LineBreakClass { get; set; }
}
+
+ internal class BiDiDataItem
+ {
+ public int Codepoint { get; set; }
+
+ public int Bracket { get; set; }
+
+ public int BracketType { get; set; }
+
+ public int BiDiClass { get; set; }
+ }
+
+
+ }
+
+ internal class UnicodeDataEntries
+ {
+ public IReadOnlyList Scripts { get; set; }
+ public IReadOnlyList GeneralCategories{ get; set; }
+ public IReadOnlyList LineBreakClasses{ get; set; }
+ }
+
+ internal class BiDiDataEntries
+ {
+ public IReadOnlyList PairedBracketTypes { get; set; }
+ public IReadOnlyList BiDiClasses{ get; set; }
}
internal readonly struct CodepointRange
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs
index 47aef84533..f122876322 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs
@@ -1,4 +1,6 @@
-using Xunit;
+using System.IO;
+using Avalonia.Media.TextFormatting.Unicode;
+using Xunit;
namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
{
@@ -11,7 +13,43 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
[Fact(Skip = "Only run when the Unicode spec changes.")]
public void Should_Generate_Data()
{
- UnicodeDataGenerator.Execute();
+ if (!Directory.Exists("Generated"))
+ {
+ Directory.CreateDirectory("Generated");
+ }
+
+ var unicodeDataTrie = UnicodeDataGenerator.GenerateUnicodeDataTrie(out var unicodeDataEntries, out var unicodeData);
+
+ foreach (var value in unicodeData.Values)
+ {
+ var data = unicodeDataTrie.Get(value.Codepoint);
+
+ Assert.Equal(value.GeneralCategory, GetValue(data, 0, UnicodeData.CATEGORY_MASK));
+
+ Assert.Equal(value.Script, GetValue(data, UnicodeData.SCRIPT_SHIFT, UnicodeData.SCRIPT_MASK));
+
+ Assert.Equal(value.LineBreakClass, GetValue(data, UnicodeData.LINEBREAK_SHIFT, UnicodeData.LINEBREAK_MASK));
+ }
+
+ var biDiTrie = UnicodeDataGenerator.GenerateBiDiTrie(out var biDiDataEntries, out var biDiData);
+
+ foreach (var value in biDiData.Values)
+ {
+ var data = biDiTrie.Get(value.Codepoint);
+
+ Assert.Equal(value.Bracket, GetValue(data, 0, UnicodeData.BIDIPAIREDBRACKED_MASK));
+
+ Assert.Equal(value.BracketType, GetValue(data, UnicodeData.BIDIPAIREDBRACKEDTYPE_SHIFT, UnicodeData.BIDIPAIREDBRACKEDTYPE_MASK));
+
+ Assert.Equal(value.BiDiClass, GetValue(data, UnicodeData.BIDICLASS_SHIFT, UnicodeData.BIDICLASS_MASK));
+ }
+
+ UnicodeEnumsGenerator.CreatePropertyValueAliasHelper(unicodeDataEntries, biDiDataEntries);
+ }
+
+ private static int GetValue(uint value, int shift, int mask)
+ {
+ return (int)((value >> shift) & mask);
}
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs
index 3a936ff3b0..e483728423 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs
@@ -270,9 +270,36 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
return entries;
}
+
+ public static List CreateBiDiPairedBracketTypeEnum()
+ {
+ var entries = new List { new DataEntry("None", "n", string.Empty) };
+
+ ParseDataEntries("# Bidi_Paired_Bracket_Type (bpt)", entries);
+
+ using (var stream = File.Create("Generated\\BiDiPairedBracketType.cs"))
+ using (var writer = new StreamWriter(stream))
+ {
+ writer.WriteLine("namespace Avalonia.Media.TextFormatting.Unicode");
+ writer.WriteLine("{");
+ writer.WriteLine(" public enum BiDiPairedBracketType");
+ writer.WriteLine(" {");
+
+ foreach (var entry in entries)
+ {
+ writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag +
+ (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment));
+ }
- public static void CreatePropertyValueAliasHelper(List scriptEntries, IEnumerable generalCategoryEntries,
- IEnumerable biDiClassEntries, IEnumerable lineBreakClassEntries)
+ writer.WriteLine(" }");
+ writer.WriteLine("}");
+ }
+
+ return entries;
+ }
+
+ public static void CreatePropertyValueAliasHelper(UnicodeDataEntries unicodeDataEntries,
+ BiDiDataEntries biDiDataEntries)
{
using (var stream = File.Create("Generated\\PropertyValueAliasHelper.cs"))
using (var writer = new StreamWriter(stream))
@@ -285,15 +312,17 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
writer.WriteLine(" internal static class PropertyValueAliasHelper");
writer.WriteLine(" {");
- WritePropertyValueAliasGetTag(writer, scriptEntries, "Script", "Zzzz");
-
- WritePropertyValueAlias(writer, scriptEntries, "Script", "Unknown");
+ WritePropertyValueAliasGetTag(writer, unicodeDataEntries.Scripts, "Script", "Zzzz");
- WritePropertyValueAlias(writer, generalCategoryEntries, "GeneralCategory", "Other");
+ WritePropertyValueAlias(writer, unicodeDataEntries.Scripts, "Script", "Unknown");
- WritePropertyValueAlias(writer, biDiClassEntries, "BiDiClass", "LeftToRight");
+ WritePropertyValueAlias(writer, unicodeDataEntries.GeneralCategories, "GeneralCategory", "Other");
+
+ WritePropertyValueAlias(writer, unicodeDataEntries.LineBreakClasses, "LineBreakClass", "Unknown");
- WritePropertyValueAlias(writer, lineBreakClassEntries, "LineBreakClass", "Unknown");
+ WritePropertyValueAlias(writer, biDiDataEntries.PairedBracketTypes, "BiDiPairedBracketType", "None");
+
+ WritePropertyValueAlias(writer, biDiDataEntries.BiDiClasses, "BiDiClass", "LeftToRight");
writer.WriteLine(" }");
writer.WriteLine("}");
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/TextNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/TextNodeTests.cs
deleted file mode 100644
index a6eaea334c..0000000000
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/TextNodeTests.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering.SceneGraph;
-using Moq;
-using Xunit;
-
-namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
-{
- public class TextNodeTests
- {
- [Fact]
- public void Bounds_Should_Be_Offset_By_Origin()
- {
- var target = new TextNode(
- Matrix.Identity,
- Brushes.Black,
- new Point(10, 10),
- Mock.Of(x => x.Bounds == new Rect(5, 5, 50, 50)));
-
- Assert.Equal(new Rect(15, 15, 50, 50), target.Bounds);
- }
- }
-}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 229bb8aef3..6100f2cb74 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -10,18 +10,6 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{
class MockRenderInterface : IPlatformRenderInterface
{
- public IFormattedTextImpl CreateFormattedText(
- string text,
- Typeface typeface,
- double fontSize,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList spans)
- {
- throw new NotImplementedException();
- }
-
public IRenderTarget CreateRenderTarget(IEnumerable surfaces)
{
throw new NotImplementedException();
diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
new file mode 100644
index 0000000000..825b1d7ea7
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png
new file mode 100644
index 0000000000..825b1d7ea7
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
new file mode 100644
index 0000000000..f7cc90678a
Binary files /dev/null and b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png
new file mode 100644
index 0000000000..f7cc90678a
Binary files /dev/null and b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png differ