diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 603308ef9a..accad63faa 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -34,9 +34,17 @@ jobs:
pool:
vmImage: 'macOS-10.14'
steps:
- - task: DotNetCoreInstaller@0
+ - task: UseDotNet@2
+ displayName: 'Use .NET Core SDK 3.0.x'
inputs:
- version: '2.1.403'
+ packageType: sdk
+ version: 3.0.x
+
+ - task: UseDotNet@2
+ displayName: 'Use .NET Core Runtime 2.1.x'
+ inputs:
+ packageType: runtime
+ version: 2.1.x
- task: CmdLine@2
displayName: 'Install Mono 5.18'
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index f8767c7599..873048ef21 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index 44d5c239ef..897e70ff81 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Avalonia
- 0.8.999
+ 0.9.999
Copyright 2019 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 796bd8e596..08a9aa3ceb 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
index f1c7664c3e..677fa2f4c5 100644
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ b/native/Avalonia.Native/inc/avalonia-native.h
@@ -92,9 +92,17 @@ enum AvnRawMouseEventType
RightButtonUp,
MiddleButtonDown,
MiddleButtonUp,
+ XButton1Down,
+ XButton1Up,
+ XButton2Down,
+ XButton2Up,
Move,
Wheel,
- NonClientLeftButtonDown
+ NonClientLeftButtonDown,
+ TouchBegin,
+ TouchUpdate,
+ TouchEnd,
+ TouchCancel
};
enum AvnRawKeyEventType
@@ -112,7 +120,9 @@ enum AvnInputModifiers
Windows = 8,
LeftMouseButton = 16,
RightMouseButton = 32,
- MiddleMouseButton = 64
+ MiddleMouseButton = 64,
+ XButton1MouseButton = 128,
+ XButton2MouseButton = 256
};
enum AvnWindowState
@@ -212,6 +222,10 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
+ virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
+ virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
+ virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
+ virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual bool TryLock() = 0;
virtual void Unlock() = 0;
};
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index 81855995b7..5c50aad4cc 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -1,16 +1,25 @@
#include "common.h"
@interface AvnAppDelegate : NSObject
@end
+
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
- [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+ if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
+ {
+ for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
+ [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
+ break;
+ }
+
+ [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+ }
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
- [NSApp activateIgnoringOtherApps:true];
+ [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
}
@end
@@ -20,5 +29,4 @@ extern void InitializeAvnApp()
NSApplication* app = [NSApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
-
}
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 0e85332555..c54829d750 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -83,6 +83,54 @@ public:
[Window setContentView: View];
}
+ virtual HRESULT ObtainNSWindowHandle(void** ret) override
+ {
+ if (ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = (__bridge void*)Window;
+
+ return S_OK;
+ }
+
+ virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override
+ {
+ if (ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = (__bridge_retained void*)Window;
+
+ return S_OK;
+ }
+
+ virtual HRESULT ObtainNSViewHandle(void** ret) override
+ {
+ if (ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = (__bridge void*)View;
+
+ return S_OK;
+ }
+
+ virtual HRESULT ObtainNSViewHandleRetained(void** ret) override
+ {
+ if (ret == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *ret = (__bridge_retained void*)View;
+
+ return S_OK;
+ }
+
virtual AvnWindow* GetNSWindow() override
{
return Window;
@@ -668,7 +716,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
AvnFramebuffer _swRenderedFrameBuffer;
bool _queuedDisplayFromThread;
NSTrackingArea* _area;
- bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isMouseOver;
+ bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
}
@@ -691,6 +739,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
self = [super init];
[self setWantsBestResolutionOpenGLSurface:true];
+ [self setWantsLayer:YES];
_parent = parent;
_area = nullptr;
return self;
@@ -893,9 +942,23 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)otherMouseDown:(NSEvent *)event
{
- _isMiddlePressed = true;
_lastMouseDownEvent = event;
- [self mouseEvent:event withType:MiddleButtonDown];
+
+ switch(event.buttonNumber)
+ {
+ case 3:
+ _isMiddlePressed = true;
+ [self mouseEvent:event withType:MiddleButtonDown];
+ break;
+ case 4:
+ _isXButton1Pressed = true;
+ [self mouseEvent:event withType:XButton1Down];
+ break;
+ case 5:
+ _isXButton2Pressed = true;
+ [self mouseEvent:event withType:XButton2Down];
+ break;
+ }
}
- (void)rightMouseDown:(NSEvent *)event
@@ -913,8 +976,21 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)otherMouseUp:(NSEvent *)event
{
- _isMiddlePressed = false;
- [self mouseEvent:event withType:MiddleButtonUp];
+ switch(event.buttonNumber)
+ {
+ case 3:
+ _isMiddlePressed = false;
+ [self mouseEvent:event withType:MiddleButtonUp];
+ break;
+ case 4:
+ _isXButton1Pressed = false;
+ [self mouseEvent:event withType:XButton1Up];
+ break;
+ case 5:
+ _isXButton2Pressed = false;
+ [self mouseEvent:event withType:XButton2Up];
+ break;
+ }
}
- (void)rightMouseUp:(NSEvent *)event
@@ -1013,6 +1089,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
rv |= MiddleMouseButton;
if (_isRightPressed)
rv |= RightMouseButton;
+ if (_isXButton1Pressed)
+ rv |= XButton1MouseButton;
+ if (_isXButton2Pressed)
+ rv |= XButton2MouseButton;
return (AvnInputModifiers)rv;
}
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 958729e2e8..52a9591c94 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -1,6 +1,4 @@
-using System;
using Avalonia;
-using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@@ -19,7 +17,7 @@ namespace ControlCatalog
desktopLifetime.MainWindow = new MainWindow();
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView();
-
+
base.OnFrameworkInitializationCompleted();
}
}
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
index 3eb6d5b595..d50b051d9f 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
@@ -1,5 +1,7 @@
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
namespace ControlCatalog.Pages
{
@@ -14,7 +16,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Find("fontComboBox");
- fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
+ fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index 103c016298..028a294492 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -15,6 +15,7 @@
+
-
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
index decd849adc..6fa9fc515e 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
_repeater = this.FindControl("repeater");
_scroller = this.FindControl("scroller");
_repeater.PointerPressed += RepeaterClick;
+ _repeater.KeyDown += RepeaterOnKeyDown;
DataContext = new ItemsRepeaterPageViewModel();
}
@@ -77,5 +78,13 @@ namespace ControlCatalog.Pages
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
}
+
+ private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.F5)
+ {
+ ((ItemsRepeaterPageViewModel)DataContext).ResetItems();
+ }
+ }
}
}
diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
index bc2ce80714..de5669123f 100644
--- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
@@ -7,25 +7,27 @@ namespace ControlCatalog.ViewModels
{
public class ItemsRepeaterPageViewModel : ReactiveObject
{
- private int newItemIndex = 1;
+ private int _newItemIndex = 1;
+ private int _newGenerationIndex = 0;
+ private ObservableCollection- _items;
public ItemsRepeaterPageViewModel()
{
- Items = new ObservableCollection
- (
- Enumerable.Range(1, 100000).Select(i => new Item
- {
- Text = $"Item {i.ToString()}",
- }));
+ Items = CreateItems();
}
- public ObservableCollection
- Items { get; }
+ public ObservableCollection
- Items
+ {
+ get => _items;
+ set => this.RaiseAndSetIfChanged(ref _items, value);
+ }
public Item SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
- Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" });
+ Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" });
}
public void RandomizeHeights()
@@ -38,6 +40,24 @@ namespace ControlCatalog.ViewModels
}
}
+ public void ResetItems()
+ {
+ Items = CreateItems();
+ }
+
+ private ObservableCollection
- CreateItems()
+ {
+ var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]";
+
+ _newGenerationIndex++;
+
+ return new ObservableCollection
- (
+ Enumerable.Range(1, 100000).Select(i => new Item
+ {
+ Text = $"Item {i.ToString()} {suffix}"
+ }));
+ }
+
public class Item : ReactiveObject
{
private double _height = double.NaN;
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 7f63e7725f..b17520a466 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -41,6 +41,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
new file mode 100644
index 0000000000..fb3e318a0e
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
new file mode 100644
index 0000000000..7f15845596
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -0,0 +1,80 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace RenderDemo.Pages
+{
+ public class GlyphRunPage : UserControl
+ {
+ private DrawingPresenter _drawingPresenter;
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
+
+ public GlyphRunPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _drawingPresenter = this.FindControl("drawingPresenter");
+
+ DispatcherTimer.Run(() =>
+ {
+ UpdateGlyphRun();
+
+ return true;
+ }, TimeSpan.FromSeconds(1));
+ }
+
+ private void UpdateGlyphRun()
+ {
+ var c = (uint)_rand.Next(65, 90);
+
+ if (_fontSize + _direction > 200)
+ {
+ _direction = -10;
+ }
+
+ if (_fontSize + _direction < 20)
+ {
+ _direction = 10;
+ }
+
+ _fontSize += _direction;
+
+ _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+ var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+
+ var drawingGroup = new DrawingGroup();
+
+ var glyphRunDrawing = new GlyphRunDrawing
+ {
+ Foreground = Brushes.Black,
+ GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
+ BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
+ };
+
+ drawingGroup.Children.Add(glyphRunDrawing);
+
+ var geometryDrawing = new GeometryDrawing
+ {
+ Pen = new Pen(Brushes.Black),
+ Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
+ };
+
+ drawingGroup.Children.Add(geometryDrawing);
+
+ _drawingPresenter.Drawing = drawingGroup;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
index d718f5917c..01daeafc3a 100644
--- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
@@ -173,12 +173,20 @@ namespace Avalonia
Contract.Requires(type != null);
Contract.Requires(name != null);
- if (name.Contains('.'))
+ if (name.Contains("."))
{
throw new InvalidOperationException("Attached properties not supported.");
}
- return GetRegistered(type).FirstOrDefault(x => x.Name == name);
+ foreach (AvaloniaProperty x in GetRegistered(type))
+ {
+ if (x.Name == name)
+ {
+ return x;
+ }
+ }
+
+ return null;
}
///
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index 44b47329ac..256de2f902 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -56,22 +55,34 @@ namespace Avalonia.Data
if (source != null)
{
+ // Perf: Avoid allocating closure in the outer scope.
+ var targetCopy = target;
+ var propertyCopy = property;
+ var bindingCopy = binding;
+
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
- .Subscribe(x => target.SetValue(property, x, binding.Priority));
+ .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
}
else
{
target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty;
}
+
case BindingMode.OneWayToSource:
+ {
+ // Perf: Avoid allocating closure in the outer scope.
+ var bindingCopy = binding;
+
return Observable.CombineLatest(
binding.Observable,
target.GetObservable(property),
(_, v) => v)
- .Subscribe(x => binding.Subject.OnNext(x));
+ .Subscribe(x => bindingCopy.Subject.OnNext(x));
+ }
+
default:
throw new ArgumentException("Invalid binding mode.");
}
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index 986e2cf012..9eec5d6b2b 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -21,6 +21,7 @@ namespace Avalonia.Data.Core
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
+ private readonly object _targetNullValue;
private readonly BindingPriority _priority;
InnerListener _innerListener;
WeakReference
- public static readonly IList PropertyAccessors =
+ public static readonly List PropertyAccessors =
new List
{
new AvaloniaPropertyAccessorPlugin(),
@@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
///
- public static readonly IList DataValidators =
+ public static readonly List DataValidators =
new List
{
new DataAnnotationsValidationPlugin(),
@@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator.
///
- public static readonly IList StreamHandlers =
+ public static readonly List StreamHandlers =
new List
{
new TaskStreamPlugin(),
diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
index ab4a109cc2..fa72235a89 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
@@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Reactive.Linq;
+using System.Runtime.ExceptionServices;
namespace Avalonia.Data.Core.Plugins
{
@@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
- private class Accessor : PropertyAccessorBase
+ private class Accessor : PropertyAccessorBase, IObserver
+
+
+
@@ -20,7 +23,7 @@
+
-
diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
new file mode 100644
index 0000000000..faf6f98138
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+using Moq;
+
+namespace Avalonia.UnitTests
+{
+ public class MockFontManagerImpl : IFontManagerImpl
+ {
+ public string GetDefaultFontFamilyName()
+ {
+ return "Default";
+ }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ return new[] { "Default" };
+ }
+
+ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
+ CultureInfo culture, out FontKey fontKey)
+ {
+ fontKey = default;
+
+ return false;
+ }
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return Mock.Of();
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
new file mode 100644
index 0000000000..93ff84d04a
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class MockGlyphTypeface : IGlyphTypefaceImpl
+ {
+ public short DesignEmHeight => 10;
+ public int Ascent => 100;
+ public int Descent => 0;
+ 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 ushort GetGlyph(uint codepoint)
+ {
+ return 0;
+ }
+
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ return new ushort[codepoints.Length];
+ }
+
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return 100;
+ }
+
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var advances = new int[glyphs.Length];
+
+ for (var i = 0; i < advances.Length; i++)
+ {
+ advances[i] = 100;
+ }
+
+ return advances;
+ }
+
+ public void Dispose() { }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 187853283f..5da9f8ff6e 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -79,9 +79,15 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ public IFontManagerImpl CreateFontManager()
{
- return Mock.Of();
+ return new MockFontManagerImpl();
+ }
+
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ width = 0;
+ return Mock.Of();
}
}
}
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index c33ec72141..a6701ef655 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -28,6 +28,10 @@ namespace Avalonia.UnitTests
return CreatePopupMock().Object;
});
+ mock.Setup(x => x.Dispose()).Callback(() =>
+ {
+ mock.Object.Closed?.Invoke();
+ });
PixelPoint pos = default;
mock.SetupGet(x => x.Position).Returns(() => pos);
mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np));
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
new file mode 100644
index 0000000000..6cbab08905
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
@@ -0,0 +1,25 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class FontManagerTests
+ {
+ [Fact]
+ public void Should_Create_Single_Instance_Typeface()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new MockPlatformRenderInterface());
+
+ var fontFamily = new FontFamily("MyFont");
+
+ var typeface = FontManager.Current.GetOrAddTypeface(fontFamily);
+
+ Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..f5e4cdc099
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
@@ -0,0 +1,130 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class GlyphRunTests : TestWithServicesBase
+ {
+ public GlyphRunTests()
+ {
+ AvaloniaLocator.CurrentMutable
+ .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)]
+ [Theory]
+ public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters))
+ {
+ var characterHit = new CharacterHit(start, trailingLength);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(expectedDistance, distance);
+ }
+ }
+
+ [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, 2, 0, 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)]
+ [Theory]
+ public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
+ int trailingLengthExpected, bool isInsideExpected)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters))
+ {
+ var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
+
+ Assert.Equal(start, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(trailingLengthExpected, textBounds.TrailingLength);
+
+ Assert.Equal(isInsideExpected, isInside);
+ }
+ }
+
+ [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)]
+ [Theory]
+ public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
+ int index, int expectedIndex, int expectedLength, double expectedWidth)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
+
+ Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(expectedLength, textBounds.TrailingLength);
+
+ Assert.Equal(expectedWidth, width, 2);
+ }
+ }
+
+ [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)]
+ [Theory]
+ public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int nextIndex, int nextLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(nextLength, characterHit.TrailingLength);
+ }
+ }
+
+ [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)]
+ [Theory]
+ public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int previousIndex, int previousLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(previousLength, characterHit.TrailingLength);
+ }
+ }
+
+ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
+ {
+ var count = glyphAdvances.Length;
+ var glyphIndices = new ushort[count];
+
+ var bounds = new Rect(0, 0, count * 10, 10);
+
+ return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
+ glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
index 6063a382a0..2061caa320 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
@@ -466,7 +466,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Once);
- context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+ context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Once);
}
@@ -496,7 +496,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
- context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Never);
+ context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Never);
context.Verify(x => x.PopOpacity(), Times.Never);
}
@@ -522,7 +522,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var animation = new BehaviorSubject(0.5);
context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
- context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+ context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacityMask(), Times.Once);
}
@@ -641,7 +641,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
var context = GetLayerContext(target, border);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
- context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+ context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Never);
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
index f57c73c45c..5fe92ba039 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
@@ -100,20 +100,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
- target.DrawRectangle(new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
+ target.DrawRectangle(Brushes.Red, new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
}
- Assert.Equal(2, node.DrawOperations.Count);
+ Assert.Equal(1, node.DrawOperations.Count);
Assert.IsType(node.DrawOperations[0].Item);
- Assert.IsType(node.DrawOperations[1].Item);
}
[Fact]
public void Should_Not_Replace_Identical_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
- var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
+ var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@@ -122,7 +120,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100));
+ target.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
@@ -135,7 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Should_Replace_Different_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
- var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
+ var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@@ -144,7 +142,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
+ target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
@@ -157,7 +155,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Should_Update_DirtyRects()
{
var node = new VisualNode(new TestRoot(), null);
- var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0);
+ var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0);
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@@ -165,7 +163,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
+ target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(new Rect(0, 0, 100, 100), layers.Single().Dirty.Single());
@@ -192,8 +190,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Green, new Rect(0, 0, 10, 100));
- target.FillRectangle(Brushes.Blue, new Rect(0, 0, 20, 100));
+ target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 10, 100));
+ target.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 20, 100));
}
Assert.Equal(2, node.DrawOperations.Count);
@@ -208,7 +206,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
public void Trimmed_DrawOperations_Releases_Reference()
{
var node = new VisualNode(new TestRoot(), null);
- var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0));
+ var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0, 0));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
@@ -218,7 +216,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
using (target.BeginUpdate(node))
{
- target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100));
+ target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs
index a8d8c07d8b..781169cfa6 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs
@@ -2,12 +2,184 @@
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.UnitTests;
+using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Visuals.UnitTests
{
public class VisualExtensionsTests
{
+ [Fact]
+ public void FindAncestorOfType_Finds_Direct_Parent()
+ {
+ StackPanel target;
+
+ var root = new TestRoot
+ {
+ Child = target = new StackPanel()
+ };
+
+ Assert.Equal(root, target.FindAncestorOfType());
+ }
+
+ [Fact]
+ public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child()
+ {
+ Button target;
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ new StackPanel
+ {
+ Children =
+ {
+ (target = new Button())
+ }
+ }
+ }
+ }
+ };
+
+ Assert.Equal(root, target.FindAncestorOfType());
+ }
+
+ [Fact]
+ public void FindDescendantOfType_Finds_Direct_Child()
+ {
+ StackPanel target;
+
+ var root = new TestRoot
+ {
+ Child = target = new StackPanel()
+ };
+
+ Assert.Equal(target, root.FindDescendantOfType());
+ }
+
+ [Fact]
+ public void FindDescendantOfType_Finds_Nested_Child()
+ {
+ Button target;
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ new StackPanel
+ {
+ Children =
+ {
+ (target = new Button())
+ }
+ }
+ }
+ }
+ };
+
+ Assert.Equal(target, root.FindDescendantOfType