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 _value; @@ -51,7 +52,7 @@ namespace Avalonia.Data.Core IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) { } @@ -63,6 +64,9 @@ namespace Avalonia.Data.Core /// /// The value to use when the binding is unable to produce a value. /// + /// + /// The value to use when the binding result is null. + /// /// The value converter to use. /// /// A parameter to pass to . @@ -72,6 +76,7 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, object fallbackValue, + object targetNullValue, IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) @@ -85,6 +90,7 @@ namespace Avalonia.Data.Core Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; _priority = priority; } @@ -196,6 +202,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) + { + return _targetNullValue; + } + if (value == BindingOperations.DoNothing) { return value; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7060fd3451..91a27be634 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core /// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. /// - 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 { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; @@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + _subscription = Instance?.GetObservable(_property).Subscribe(this); } protected override void UnsubscribeCore() @@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins _subscription?.Dispose(); _subscription = null; } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + void IObserver.OnNext(object value) + { + PublishValue(value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 70f53b8b88..45b7207413 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -2,8 +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.Linq; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -41,7 +39,17 @@ namespace Avalonia.Data.Core { reference.TryGetTarget(out object target); - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + IPropertyAccessorPlugin plugin = null; + + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + plugin = x; + break; + } + } + var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs new file mode 100644 index 0000000000..837ea1abcd --- /dev/null +++ b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs @@ -0,0 +1,15 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Platform +{ + public interface IMacOSTopLevelPlatformHandle + { + IntPtr NSView { get; } + IntPtr GetNSViewRetained(); + IntPtr NSWindow { get; } + IntPtr GetNSWindowRetained(); + } +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 41009e4cd3..f5052e5858 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -116,20 +116,33 @@ namespace Avalonia.Reactive { if (Volatile.Read(ref _observers) != null) { - IObserver[] observers; - + IObserver[] observers = null; + IObserver singleObserver = null; lock (this) { if (_observers == null) { return; } - observers = _observers.ToArray(); + if (_observers.Count == 1) + { + singleObserver = _observers[0]; + } + else + { + observers = _observers.ToArray(); + } } - - foreach (var observer in observers) + if (singleObserver != null) { - observer.OnNext(value); + singleObserver.OnNext(value); + } + else + { + foreach (var observer in observers) + { + observer.OnNext(value); + } } } } diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..973b0aa641 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -15,7 +15,7 @@ namespace Avalonia.Utilities { if (IsValidIdentifierStart(r.Peek)) { - return r.TakeWhile(IsValidIdentifierChar); + return r.TakeWhile(c => IsValidIdentifierChar(c)); } else { diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs new file mode 100644 index 0000000000..a1246c57b5 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _readOnlyList; + private int _pos; + + public ImmutableReadOnlyListStructEnumerator(IReadOnlyList readOnlyList) + { + _readOnlyList = readOnlyList; + _pos = -1; + Current = default; + } + + public T Current + { + get; + private set; + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + if (_pos >= _readOnlyList.Count - 1) + { + return false; + } + + Current = _readOnlyList[++_pos]; + + return true; + + } + + public void Reset() + { + _pos = -1; + + Current = default; + } + } +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 59c6c47ed9..9158ac7038 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -45,6 +45,12 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + StyledElement.DataContextProperty.AddOwner(); + /// public event EventHandler ResourcesChanged; @@ -56,6 +62,19 @@ namespace Avalonia Name = "Avalonia Application"; } + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs index e2e591215e..5c931bc771 100644 --- a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs @@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators /// Gets the container index for the tree. /// TreeContainerIndex Index { get; } + + /// + /// Updates the index based on the parent . + /// + void UpdateIndex(); } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index c06a64443c..9200490668 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Generators { @@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { + private TreeView _treeView; + /// /// Initializes a new instance of the class. /// @@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators /// The container's ContentTemplate property. /// The container's Items property. /// The container's IsExpanded property. - /// The container index for the tree public TreeItemContainerGenerator( IControl owner, AvaloniaProperty contentProperty, AvaloniaProperty contentTemplateProperty, AvaloniaProperty itemsProperty, - AvaloniaProperty isExpandedProperty, - TreeContainerIndex index) + AvaloniaProperty isExpandedProperty) : base(owner, contentProperty, contentTemplateProperty) { Contract.Requires(owner != null); Contract.Requires(contentProperty != null); Contract.Requires(itemsProperty != null); Contract.Requires(isExpandedProperty != null); - Contract.Requires(index != null); ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; - Index = index; + UpdateIndex(); } /// /// Gets the container index for the tree. /// - public TreeContainerIndex Index { get; } + public TreeContainerIndex Index { get; private set; } /// /// Gets the item container's Items property. @@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators } else if (container != null) { - Index.Add(item, container); + Index?.Add(item, container); return container; } else @@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - Index.Add(item, result); + Index?.Add(item, result); return result; } @@ -101,24 +102,50 @@ namespace Avalonia.Controls.Generators public override IEnumerable Clear() { var items = base.Clear(); - Index.Remove(0, items); + Index?.Remove(0, items); return items; } public override IEnumerable Dematerialize(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } public override IEnumerable RemoveRange(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.RemoveRange(startingIndex, count); } public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + public void UpdateIndex() + { + if (Owner is TreeView treeViewOwner && Index == null) + { + Index = new TreeContainerIndex(); + _treeView = treeViewOwner; + } + else if (Owner.IsAttachedToLogicalTree) + { + var treeView = Owner.GetSelfAndLogicalAncestors().OfType().FirstOrDefault(); + + if (treeView != _treeView) + { + Clear(); + Index = treeView?.ItemContainerGenerator?.Index; + _treeView = treeView; + } + } + else + { + Clear(); + Index = null; + _treeView = null; + } + } + class WrapperTreeDataTemplate : ITreeDataTemplate { private readonly IDataTemplate _inner; diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bf22f0a08a..94c34b28d6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -324,20 +323,24 @@ namespace Avalonia.Controls return; } - var current = focus.Current - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.VisualParent == container); + IVisual current = focus.Current; - if (current != null) + while (current != null) { - var next = GetNextControl(container, direction.Value, current, false); - - if (next != null) + if (current.VisualParent == container && current is IInputElement inputElement) { - focus.Focus(next, NavigationMethod.Directional); - e.Handled = true; + IInputElement next = GetNextControl(container, direction.Value, inputElement, false); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + + break; } + + current = current.VisualParent; } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index aa91224572..1a9347e317 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications /// The that will be the host. private void Install(Window host) { - var adornerLayer = host.GetVisualDescendants() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; - if (adornerLayer != null) - { - adornerLayer.Children.Add(this); - } + adornerLayer?.Children.Add(this); } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index cd14211075..d27de7a80d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -188,8 +188,8 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Remove: - if (e.OldStartingIndex >= FirstIndex && - e.OldStartingIndex < NextIndex) + if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) || + panel.Children.Count > ItemCount) { RecycleContainersOnRemove(); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e0cc9aa128..9084012619 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index f1ee7c0e1a..baa51f92ec 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Minimum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Maximum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Value"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double value is NaN or Inf. + /// Checks if the double value is not inifinity nor NaN. /// /// The value. - /// The name of the property being set. - private static void ValidateDouble(double value, string property) + private static bool ValidateDouble(double value) { - if (double.IsInfinity(value) || double.IsNaN(value)) - { - throw new ArgumentException($"{value} is not a valid value for {property}."); - } + return !double.IsInfinity(value) || !double.IsNaN(value); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 329b086a7c..a5bbcec186 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Logging; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives /// The container or null if the event did not originate in a container. protected IControl GetContainerFromEventSource(IInteractive eventSource) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); + var parent = (IVisual)eventSource; - return item; + while (parent != null) + { + if (parent is IControl control && control.LogicalParent == this + && ItemContainerGenerator?.IndexFromContainer(control) != -1) + { + return control; + } + + parent = parent.VisualParent; + } + + return null; } /// diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 257c1b2399..0e2136a6f3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -565,7 +565,17 @@ namespace Avalonia.Controls if (Layout is VirtualizingLayout virtualLayout) { var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } } else if (Layout is NonVirtualizingLayout nonVirtualLayout) { diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 499dfb5320..7728302aad 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.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.Reflection; using Avalonia.Collections; using Avalonia.Media; @@ -166,12 +165,9 @@ namespace Avalonia.Controls.Shapes { property.Changed.Subscribe(e => { - var senderType = e.Sender.GetType().GetTypeInfo(); - var affectedType = typeof(TShape).GetTypeInfo(); - - if (affectedType.IsAssignableFrom(senderType)) + if (e.Sender is TShape shape) { - AffectsGeometryInvalidate(e); + AffectsGeometryInvalidate(shape, e); } }); } @@ -322,13 +318,8 @@ namespace Avalonia.Controls.Shapes return (size, transform); } - private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e) + private static void AffectsGeometryInvalidate(Shape control, AvaloniaPropertyChangedEventArgs e) { - if (!(e.Sender is Shape control)) - { - return; - } - // If the geometry is invalidated when Bounds changes, only invalidate when the Size // portion changes. if (e.Property == BoundsProperty) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c7855ddfd1..8b8c7285be 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,7 +352,7 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..5a8711a21c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // 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.Linq; -using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,7 +13,6 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -269,6 +266,12 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + var logicalArgs = new LogicalTreeAttachmentEventArgs(this); + ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); + + var visualArgs = new VisualTreeAttachmentEventArgs(this, this); + OnDetachedFromVisualTreeCore(visualArgs); + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); @@ -296,10 +299,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) - { - control.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); } /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 59844be8a6..738d9d0b51 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -393,8 +393,7 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); result.Index.Materialized += ContainerMaterialized; return result; } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 07d5497c14..4d24337c3a 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -98,17 +98,18 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); } /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); + _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); - + Level = CalculateDistanceFromLogicalParent(this) - 1; + ItemContainerGenerator.UpdateIndex(); if (ItemTemplate == null && _treeView?.ItemTemplate != null) { @@ -119,7 +120,7 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); - ItemContainerGenerator.Clear(); + ItemContainerGenerator.UpdateIndex(); } protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index a91e6a3a48..d6cf3df536 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -90,23 +90,17 @@ namespace Avalonia.Controls.Utils { var borderThickness = borders.Top; var top = borderThickness * 0.5; - var cornerRadius = (float)Math.Max(0, radii.TopLeft - borderThickness - top); - if (background != null) - { - var topLeft = new Point(borders.Left, borders.Top); - var bottomRight = new Point(size.Width - borders.Right, size.Height - borders.Bottom); - var innerRect = new Rect(topLeft, bottomRight); - context.FillRectangle(background, innerRect, cornerRadius); - } + IPen pen = null; - if (borderBrush != null && borderThickness > 0) + if (borderThickness > 0) { - var topLeft = new Point(top, top); - var bottomRight = new Point(size.Width - top, size.Height - top); - var outerRect = new Rect(topLeft, bottomRight); - context.DrawRectangle(new Pen(borderBrush, borderThickness), outerRect, (float)radii.TopLeft); + pen = new Pen(borderBrush, borderThickness); } + + var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness); + + context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1816a6c81d..f66a248aaf 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -336,7 +336,6 @@ namespace Avalonia.Controls if (close) { PlatformImpl?.Dispose(); - HandleClosed(); } } } diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 104ac9cb61..77902a7390 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,18 +180,18 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); + IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; - if (element == null || !CanFocus(element)) + while (element != null) { - element = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(CanFocus); - } + if (element is IInputElement inputElement && CanFocus(inputElement)) + { + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); - if (element != null) - { - Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + break; + } + + element = element.VisualParent; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 1a82f7d671..144979523f 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -48,6 +48,8 @@ namespace Avalonia.Input LeftMouseButton = 16, RightMouseButton = 32, MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 535b930f8b..1e2a621bd1 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -342,7 +342,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the control is focused. + /// Gets a value indicating whether the control is focused. /// public bool IsFocused { @@ -360,7 +360,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the pointer is currently over the control. + /// Gets a value indicating whether the pointer is currently over the control. /// public bool IsPointerOver { diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 4dcf0eee53..c74498171e 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -120,6 +120,10 @@ namespace Avalonia.Input rv++; if (props.IsRightButtonPressed) rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; return rv; } @@ -142,6 +146,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: if (ButtonCount(props) > 1) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -151,6 +157,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: if (ButtonCount(props) != 0) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -186,12 +194,20 @@ namespace Avalonia.Input kind = PointerUpdateKind.MiddleButtonPressed; if (args.Type == RawPointerEventType.RightButtonDown) kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; if (args.Type == RawPointerEventType.MiddleButtonUp) kind = PointerUpdateKind.MiddleButtonReleased; if (args.Type == RawPointerEventType.RightButtonUp) kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; return new PointerPointProperties(args.InputModifiers, kind); } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 819d231b31..00222e92cf 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -55,9 +55,11 @@ namespace Avalonia.Input Captured.DetachedFromVisualTree += OnCaptureDetached; } - IInputElement GetNextCapture(IVisual parent) => - parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); - + IInputElement GetNextCapture(IVisual parent) + { + return parent as IInputElement ?? parent.FindAncestorOfType(); + } + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) { Capture(GetNextCapture(e.Parent)); diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 1068a0d4d4..96132fdc90 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -21,18 +21,24 @@ namespace Avalonia.Input public bool IsLeftButtonPressed { get; } public bool IsMiddleButtonPressed { get; } public bool IsRightButtonPressed { get; } + public bool IsXButton1Pressed { get; } + public bool IsXButton2Pressed { get; } + public PointerUpdateKind PointerUpdateKind { get; } + private PointerPointProperties() - { - + { } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; + IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton); IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton); IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton); + IsXButton1Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton1MouseButton); + IsXButton2Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton2MouseButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state @@ -49,6 +55,14 @@ namespace Avalonia.Input IsRightButtonPressed = true; if (kind == PointerUpdateKind.RightButtonReleased) IsRightButtonPressed = false; + if (kind == PointerUpdateKind.XButton1Pressed) + IsXButton1Pressed = true; + if (kind == PointerUpdateKind.XButton1Released) + IsXButton1Pressed = false; + if (kind == PointerUpdateKind.XButton2Pressed) + IsXButton2Pressed = true; + if (kind == PointerUpdateKind.XButton2Released) + IsXButton2Pressed = false; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -59,9 +73,13 @@ namespace Avalonia.Input LeftButtonPressed, MiddleButtonPressed, RightButtonPressed, + XButton1Pressed, + XButton2Pressed, LeftButtonReleased, MiddleButtonReleased, RightButtonReleased, + XButton1Released, + XButton2Released, Other } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 56854c7d29..4b2c95943d 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -14,6 +14,10 @@ namespace Avalonia.Input.Raw RightButtonUp, MiddleButtonDown, MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, Move, Wheel, NonClientLeftButtonDown, diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index cfb4b14b1c..c235bcd90f 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.VisualTree; namespace Avalonia.Layout { @@ -61,5 +62,31 @@ namespace Avalonia.Layout return availableSize; } + + /// + /// Invalidates measure for given control and all visual children recursively. + /// + public static void InvalidateSelfAndChildrenMeasure(ILayoutable control) + { + void InnerInvalidateMeasure(IVisual target) + { + if (target is ILayoutable targetLayoutable) + { + targetLayoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } + } + + InnerInvalidateMeasure(control); + } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 4732808b91..9d0a5c57ee 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.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 Avalonia.Logging; using Avalonia.VisualTree; @@ -694,12 +693,9 @@ namespace Avalonia.Layout } /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) - { - i.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 88a6da8945..65c2a75b1c 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -22,6 +22,6 @@ - + diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 8b397403ca..e72fefe3ce 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -16,6 +16,34 @@ using Avalonia.Threading; namespace Avalonia.Native { + public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle + { + IAvnWindowBase _native; + + public MacOSTopLevelWindowHandle(IAvnWindowBase native) + { + _native = native; + } + + public IntPtr Handle => NSWindow; + + public string HandleDescriptor => "NSWindow"; + + public IntPtr NSView => _native.ObtainNSViewHandle(); + + public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + + public IntPtr GetNSViewRetained() + { + return _native.ObtainNSViewHandleRetained(); + } + + public IntPtr GetNSWindowRetained() + { + return _native.ObtainNSWindowHandleRetained(); + } + } + public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface { @@ -45,6 +73,9 @@ namespace Avalonia.Native protected void Init(IAvnWindowBase window, IAvnScreens screens) { _native = window; + + Handle = new MacOSTopLevelWindowHandle(window); + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; @@ -349,6 +380,6 @@ namespace Avalonia.Native } - public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED"); + public IPlatformHandle Handle { get; private set; } } } diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs new file mode 100644 index 0000000000..31639c5784 --- /dev/null +++ b/src/Avalonia.Styling/IDataContextProvider.cs @@ -0,0 +1,13 @@ +namespace Avalonia +{ + /// + /// Defines an element with a data context that can be used for binding. + /// + public interface IDataContextProvider : IAvaloniaObject + { + /// + /// Gets or sets the element's data context. + /// + object DataContext { get; set; } + } +} diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index bcf1898c4c..d4d0f179c3 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -10,7 +10,8 @@ namespace Avalonia IStyleHost, ILogical, IResourceProvider, - IResourceNode + IResourceNode, + IDataContextProvider { /// /// Occurs when the control has finished initialization. @@ -27,11 +28,6 @@ namespace Avalonia /// new Classes Classes { get; set; } - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - /// /// Gets the control's logical parent. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index de8093c048..5e1bcde2f6 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -24,7 +24,7 @@ namespace Avalonia /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// - public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. @@ -288,7 +288,7 @@ namespace Avalonia var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, - Validate = ValidateLogicalChild + Validate = logical => ValidateLogicalChild(logical) }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index e7cbed8ec5..4cdb116cdd 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -13,7 +13,6 @@ Height="18" VerticalAlignment="Center"/> + - \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs new file mode 100644 index 0000000000..978a5b0c4c --- /dev/null +++ b/src/Avalonia.Visuals/Media/CharacterHit.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Media +{ + /// + /// Represents information about a character hit within a glyph run. + /// + /// + /// The CharacterHit structure provides information about the index of the first + /// character that got hit as well as information about leading or trailing edge. + /// + public readonly struct CharacterHit : IEquatable + { + /// + /// Initializes a new instance of the structure. + /// + /// Index of the first character that got hit. + /// In the case of a leading edge, this value is 0. In the case of a trailing edge, + /// this value is the number of code points until the next valid caret position. + public CharacterHit(int firstCharacterIndex, int trailingLength = 0) + { + FirstCharacterIndex = firstCharacterIndex; + + TrailingLength = trailingLength; + } + + /// + /// Gets the index of the first character that got hit. + /// + public int FirstCharacterIndex { get; } + + /// + /// Gets the trailing length value for the character that got hit. + /// + public int TrailingLength { get; } + + public bool Equals(CharacterHit other) + { + return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength; + } + + public override bool Equals(object obj) + { + return obj is CharacterHit other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return FirstCharacterIndex * 397 ^ TrailingLength; + } + } + + public static bool operator ==(CharacterHit left, CharacterHit right) + { + return left.Equals(right); + } + + public static bool operator !=(CharacterHit left, CharacterHit right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4c9bf9ebd4..df69ab6fd5 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -118,6 +118,42 @@ namespace Avalonia.Media } } + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (Math.Abs(radiusX) > double.Epsilon) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (Math.Abs(radiusY) > double.Epsilon) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY); + } + /// /// Draws the outline of a rectangle. /// @@ -126,10 +162,7 @@ namespace Avalonia.Media /// The corner radius. public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) { - if (PenIsVisible(pen)) - { - PlatformImpl.DrawRectangle(pen, rect, cornerRadius); - } + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); } /// @@ -154,6 +187,22 @@ namespace Avalonia.Media } } + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + /// The baseline origin of the glyph run. + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + Contract.Requires(glyphRun != null); + + if (foreground != null) + { + PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + } + } + /// /// Draws a filled rectangle. /// @@ -162,10 +211,7 @@ namespace Avalonia.Media /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) { - if (brush != null && rect != Rect.Empty) - { - PlatformImpl.FillRectangle(brush, rect, cornerRadius); - } + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); } public readonly struct PushedState : IDisposable diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index b57b4a0ca8..a69a93e416 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -2,17 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.Media.Fonts; namespace Avalonia.Media { public sealed class FontFamily { + public const string DefaultFontFamilyName = "$Default"; + static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(DefaultFontFamilyName); } /// @@ -57,15 +57,6 @@ namespace Avalonia.Media /// public static FontFamily Default { get; } - /// - /// Represents all font families in the system. This can be an expensive call depending on platform implementation. - /// - /// - /// Consider using the new instead. - /// - public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); - /// /// Gets the primary family name of the font family. /// @@ -86,10 +77,16 @@ namespace Avalonia.Media /// Gets the key for associated assets. /// /// - /// The family familyNames. + /// The family key. /// + /// Key is only used for custom fonts. public FontFamilyKey Key { get; } + /// + /// Returns True if this instance is the system's default. + /// + public bool IsDefault => Name.Equals(DefaultFontFamilyName); + /// /// Implicit conversion of string to FontFamily /// @@ -184,36 +181,40 @@ namespace Avalonia.Media { unchecked { - var hash = (int)2186146271; - - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } - else - { - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); - } + return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + } + } - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } + public static bool operator !=(FontFamily a, FontFamily b) + { + return !(a == b); + } - return hash; + public static bool operator ==(FontFamily a, FontFamily b) + { + if (ReferenceEquals(a, b)) + { + return true; } + + return !(a is null) && a.Equals(b); } public override bool Equals(object obj) { + if (ReferenceEquals(this, obj)) + { + return true; + } + if (!(obj is FontFamily other)) { return false; } - if (Key != null) + if (!Equals(Key, other.Key)) { - return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key); + return false; } return other.FamilyNames.Equals(FamilyNames); diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index be1bd269ed..0c5e88b47a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.Media @@ -11,9 +13,53 @@ namespace Avalonia.Media /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts. /// It is also responsible for the font fallback. /// - public abstract class FontManager + public sealed class FontManager { - public static readonly FontManager Default = CreateDefault(); + private readonly ConcurrentDictionary _typefaceCache = + new ConcurrentDictionary(); + private readonly FontFamily _defaultFontFamily; + + private FontManager(IFontManagerImpl platformImpl) + { + PlatformImpl = platformImpl; + + DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + } + + public static FontManager Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + var renderInterface = AvaloniaLocator.Current.GetService(); + + var fontManagerImpl = renderInterface?.CreateFontManager(); + + if (fontManagerImpl == null) + { + return null; + } + + current = new FontManager(fontManagerImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// + /// + public IFontManagerImpl PlatformImpl { get; } /// /// Gets the system's default font family's name. @@ -21,25 +67,55 @@ namespace Avalonia.Media public string DefaultFontFamilyName { get; - protected set; } /// - /// Get all installed fonts in the system. - /// If true the font collection is updated. + /// Get all installed font family names. /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// If true the font collection is updated. + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Get a cached typeface from specified parameters. + /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. /// The font weight. /// The font style. /// - /// The cached typeface. + /// The typeface. /// - public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + { + while (true) + { + if (fontFamily.IsDefault) + { + fontFamily = _defaultFontFamily; + } + + var key = new FontKey(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryGetValue(key, out var typeface)) + { + return typeface; + } + + typeface = new Typeface(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryAdd(key, typeface)) + { + return typeface; + } + + if (fontFamily == _defaultFontFamily) + { + return null; + } + + fontFamily = _defaultFontFamily; + } + } /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -53,60 +129,13 @@ namespace Avalonia.Media /// /// The matched typeface. /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); - - public static FontManager CreateDefault() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if (platformImpl != null) - { - return new PlatformFontManager(platformImpl); - } - - return new EmptyFontManager(); - } - - private class PlatformFontManager : FontManager + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + FontStyle fontStyle = FontStyle.Normal, + FontFamily fontFamily = null, CultureInfo culture = null) { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManager(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - } - - private class EmptyFontManager : FontManager - { - public EmptyFontManager() - { - DefaultFontFamilyName = "Empty"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - new[] { DefaultFontFamilyName }; - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => null; + return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + null; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index e777d93315..cd08bba7b2 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { @@ -54,25 +55,19 @@ namespace Avalonia.Media.Fonts /// internal IReadOnlyList Names { get; } - /// /// - /// Returns an enumerator that iterates through the collection. + /// Returns an enumerator for the name collection. /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + public ImmutableReadOnlyListStructEnumerator GetEnumerator() { - return Names.GetEnumerator(); + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -111,7 +106,39 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - return ToString().GetHashCode(); + if (Count == 0) + { + return 0; + } + + unchecked + { + int hash = 17; + + for (var i = 0; i < Names.Count; i++) + { + string name = Names[i]; + + hash = hash * 23 + name.GetHashCode(); + } + + return hash; + } + } + + public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b) + { + return !(a == b); + } + + public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); } /// @@ -128,7 +155,20 @@ namespace Avalonia.Media.Fonts return false; } - return other.ToString().Equals(ToString()); + if (other.Count != Count) + { + return false; + } + + for (int i = 0; i < Count; i++) + { + if (Names[i] != other.Names[i]) + { + return false; + } + } + + return true; } public int Count => Names.Count; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 7733dd7d2a..887862face 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts } } + public static bool operator !=(FontFamilyKey a, FontFamilyKey b) + { + return !(a == b); + } + + public static bool operator ==(FontFamilyKey a, FontFamilyKey b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index 063fe8f20d..bed1fc6b83 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts { public static class FontFamilyLoader { - private static readonly IAssetLoader s_assetLoader; - - static FontFamilyLoader() - { - s_assetLoader = AvaloniaLocator.Current.GetService(); - } - /// /// Loads all font assets that belong to the specified /// @@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var assetLoader = AvaloniaLocator.Current.GetService(); + + var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); var matchingAssets = availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); @@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var assetLoader = AvaloniaLocator.Current.GetService(); + var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); - var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); string compareTo; diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs similarity index 65% rename from src/Skia/Avalonia.Skia/FontKey.cs rename to src/Avalonia.Visuals/Media/Fonts/FontKey.cs index bb3fe230c1..0ead585612 100644 --- a/src/Skia/Avalonia.Skia/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -2,24 +2,26 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Media; -namespace Avalonia.Skia +namespace Avalonia.Media.Fonts { - internal readonly struct FontKey : IEquatable + public readonly struct FontKey : IEquatable { + public readonly FontFamily FontFamily; public readonly FontStyle Style; public readonly FontWeight Weight; - public FontKey(FontWeight weight, FontStyle style) + public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) { + FontFamily = fontFamily; Style = style; Weight = weight; } public override int GetHashCode() { - var hash = 17; + var hash = FontFamily.GetHashCode(); + hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -33,7 +35,8 @@ namespace Avalonia.Skia public bool Equals(FontKey other) { - return Style == other.Style && + return FontFamily == other.FontFamily && + Style == other.Style && Weight == other.Weight; } } diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs new file mode 100644 index 0000000000..43151deece --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -0,0 +1,457 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Utility; + +namespace Avalonia.Media +{ + /// + /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style. + /// + public sealed class GlyphRun : IDisposable + { + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + private IGlyphRunImpl _glyphRunImpl; + private GlyphTypeface _glyphTypeface; + private double _fontRenderingEmSize; + private Rect? _bounds; + + private ReadOnlySlice _glyphIndices; + private ReadOnlySlice _glyphAdvances; + private ReadOnlySlice _glyphOffsets; + private ReadOnlySlice _glyphClusters; + private ReadOnlySlice _characters; + + /// + /// Initializes a new instance of the class. + /// + public GlyphRun() + { + + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The glyph indices. + /// The glyph advances. + /// The glyph offsets. + /// The characters. + /// The glyph clusters. + /// The bidi level. + /// The bound. + public GlyphRun( + GlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlySlice glyphIndices, + ReadOnlySlice glyphAdvances = default, + ReadOnlySlice glyphOffsets = default, + ReadOnlySlice characters = default, + ReadOnlySlice glyphClusters = default, + int bidiLevel = 0, + Rect? bounds = null) + { + GlyphTypeface = glyphTypeface; + + FontRenderingEmSize = fontRenderingEmSize; + + GlyphIndices = glyphIndices; + + GlyphAdvances = glyphAdvances; + + GlyphOffsets = glyphOffsets; + + Characters = characters; + + GlyphClusters = glyphClusters; + + BidiLevel = bidiLevel; + + Initialize(bounds); + } + + /// + /// Gets or sets the for the . + /// + public GlyphTypeface GlyphTypeface + { + get => _glyphTypeface; + set => Set(ref _glyphTypeface, value); + } + + /// + /// Gets or sets the em size used for rendering the . + /// + public double FontRenderingEmSize + { + get => _fontRenderingEmSize; + set => Set(ref _fontRenderingEmSize, value); + } + + /// + /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. + /// + public ReadOnlySlice GlyphIndices + { + get => _glyphIndices; + set => Set(ref _glyphIndices, value); + } + + /// + /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. + /// + public ReadOnlySlice GlyphAdvances + { + get => _glyphAdvances; + set => Set(ref _glyphAdvances, value); + } + + /// + /// Gets or sets an array of values representing the offsets of the glyphs in the . + /// + public ReadOnlySlice GlyphOffsets + { + get => _glyphOffsets; + set => Set(ref _glyphOffsets, value); + } + + /// + /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . + /// + public ReadOnlySlice Characters + { + get => _characters; + set => Set(ref _characters, value); + } + + /// + /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// + public ReadOnlySlice GlyphClusters + { + get => _glyphClusters; + set => Set(ref _glyphClusters, value); + } + + /// + /// Gets or sets the bidirectional nesting level of the . + /// + public int BidiLevel + { + get; + set; + } + + /// + /// + /// + internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + /// + /// + /// + internal bool IsLeftToRight => ((BidiLevel & 1) == 0); + + /// + /// Gets or sets the conservative bounding box of the . + /// + public Rect Bounds + { + get + { + if (_bounds == null) + { + _bounds = CalculateBounds(); + } + + return _bounds.Value; + } + set => _bounds = value; + } + + public IGlyphRunImpl GlyphRunImpl + { + get + { + if (_glyphRunImpl == null) + { + Initialize(null); + } + + return _glyphRunImpl; + } + } + + public double GetDistanceFromCharacterHit(CharacterHit characterHit) + { + var distance = 0.0; + + var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + for (var i = 0; i < _glyphClusters.Length; i++) + { + if (_glyphClusters[i] >= end) + { + break; + } + + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[i]; + + distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + distance += GlyphAdvances[i]; + } + } + + return distance; + } + + public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) + { + // Before + if (distance < 0) + { + isInside = false; + + var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _); + + return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; + } + + //After + if (distance > Bounds.Size.Width) + { + isInside = false; + + var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _); + + return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); + } + + //Within + var currentX = 0.0; + var index = 0; + + for (; index < GlyphIndices.Length; index++) + { + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[index]; + + currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + currentX += GlyphAdvances[index]; + } + + if (currentX > distance) + { + break; + } + } + + if (index == GlyphIndices.Length) + { + index--; + } + + var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); + + isInside = distance < currentX && width > 0; + + var isTrailing = distance > currentX - width / 2; + + return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex); + } + + public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) + { + if (characterHit.TrailingLength == 0) + { + return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); + } + + var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + return new CharacterHit(nextCharacterHit.FirstCharacterIndex); + } + + public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + return characterHit.TrailingLength == 0 ? + FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) : + new CharacterHit(characterHit.FirstCharacterIndex); + } + + private class ReverseComparer : IComparer + { + public int Compare(T x, T y) + { + return Comparer.Default.Compare(y, x); + } + } + + private static readonly IComparer s_ascendingComparer = Comparer.Default; + private static readonly IComparer s_descendingComparer = new ReverseComparer(); + + internal CharacterHit FindNearestCharacterHit(int index, out double width) + { + width = 0.0; + + if (index < 0) + { + return default; + } + + var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; + + var clusters = _glyphClusters.AsSpan(); + + int start; + + if (index == 0 && clusters[0] == 0) + { + start = 0; + } + else + { + // Find the start of the cluster at the character index. + start = clusters.BinarySearch((ushort)index, comparer); + } + + // No cluster found. + if (start < 0) + { + while (index > 0 && start < 0) + { + index--; + + start = clusters.BinarySearch((ushort)index, comparer); + } + + if (start < 0) + { + return default; + } + } + + var trailingLength = 0; + + var currentCluster = clusters[start]; + + while (start > 0 && clusters[start - 1] == currentCluster) + { + start--; + } + + for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex) + { + if (_glyphClusters[lastIndex] != currentCluster) + { + break; + } + + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[lastIndex]; + + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + width += GlyphAdvances[lastIndex]; + } + + trailingLength++; + } + + return new CharacterHit(currentCluster, trailingLength); + } + + private Rect CalculateBounds() + { + var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale; + + var width = 0.0; + + if (GlyphAdvances.IsEmpty) + { + foreach (var glyph in GlyphIndices) + { + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + } + else + { + foreach (var advance in GlyphAdvances) + { + width += advance; + } + } + + return new Rect(0, 0, width, height); + } + + private void Set(ref T field, T value) + { + if (_glyphRunImpl != null) + { + throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'"); + } + + field = value; + } + + private void Initialize(Rect? bounds) + { + if (GlyphIndices.Length == 0) + { + throw new InvalidOperationException(); + } + + var glyphCount = GlyphIndices.Length; + + if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + _glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width); + + if (bounds.HasValue) + { + _bounds = bounds; + } + else + { + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _bounds = new Rect(0, 0, width, height); + } + } + + void IDisposable.Dispose() + { + _glyphRunImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs new file mode 100644 index 0000000000..22d6e20b34 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -0,0 +1,50 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Media +{ + public class GlyphRunDrawing : Drawing + { + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); + + public static readonly StyledProperty BaselineOriginProperty = + AvaloniaProperty.Register(nameof(BaselineOrigin)); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public GlyphRun GlyphRun + { + get => GetValue(GlyphRunProperty); + set => SetValue(GlyphRunProperty, value); + } + + public Point BaselineOrigin + { + get => GetValue(BaselineOriginProperty); + set => SetValue(BaselineOriginProperty, value); + } + + public override void Draw(DrawingContext context) + { + if (GlyphRun == null) + { + return; + } + + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + public override Rect GetBounds() + { + return GlyphRun?.Bounds ?? default; + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 3ba31f7e84..6468f701d6 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,11 +9,9 @@ namespace Avalonia.Media { public sealed class GlyphTypeface : IDisposable { - private static readonly IPlatformRenderInterface s_platformRenderInterface = - AvaloniaLocator.Current.GetService(); - - public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) + public GlyphTypeface(Typeface typeface) { + PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface); } public GlyphTypeface(IGlyphTypefaceImpl platformImpl) @@ -68,6 +66,11 @@ namespace Avalonia.Media /// public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + public bool IsFixedPitch => PlatformImpl.IsFixedPitch; + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index a6d5c8a43c..9a17bad7d2 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -13,8 +13,6 @@ namespace Avalonia.Media [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] public class Typeface : IEquatable { - public static readonly Typeface Default = new Typeface(FontFamily.Default); - private GlyphTypeface _glyphTypeface; /// @@ -50,6 +48,8 @@ namespace Avalonia.Media { } + public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + /// /// Gets the font family. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index f74c551fe0..f2309c271d 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -61,12 +61,22 @@ namespace Avalonia.Platform void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// - /// Draws the outline of a rectangle. + /// Draws a rectangle with the specified Brush and Pen. /// - /// The pen. + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. - /// The corner radius. - void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0); /// /// Draws text. @@ -77,12 +87,12 @@ namespace Avalonia.Platform void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text); /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f); + /// The foreground. + /// The glyph run. + /// The baseline origin of the glyph run. + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 254b5d07d1..a8e6dcb29b 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -12,7 +13,7 @@ namespace Avalonia.Platform /// /// Gets the system's default font family's name. /// - string DefaultFontFamilyName { get; } + string GetDefaultFontFamilyName(); /// /// Get all installed fonts in the system. @@ -20,17 +21,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Get a typeface from specified parameters. - /// - /// The font family. - /// The font weight. - /// The font style. - /// - /// The typeface. - /// - Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - /// /// Tries to match a specified character to a typeface that supports specified font properties. /// @@ -39,10 +29,20 @@ namespace Avalonia.Platform /// The font style. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching font key. /// - /// The typeface. + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + + /// + /// Creates a glyph typeface. + /// + /// The typeface. + /// 0 + /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. /// - Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs new file mode 100644 index 0000000000..0f1359794a --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs @@ -0,0 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Platform +{ + /// + /// Actual implementation of a glyph run that stores platform dependent resources. + /// + public interface IGlyphRunImpl : IDisposable { } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs index 8c043a5129..5d6ff23c0a 100644 --- a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -47,6 +47,11 @@ namespace Avalonia.Platform /// int StrikethroughThickness { get; } + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + bool IsFixedPitch { get; } + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 5a0a7b2f19..7ae0eaf8f2 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -113,12 +113,17 @@ namespace Avalonia.Platform IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); /// - /// Creates a glyph typeface for specified typeface. + /// Creates a font manager implementation. /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + /// The font manager. + IFontManagerImpl CreateFontManager(); + + /// + /// Creates a platform implementation of a glyph run. + /// + /// The glyph run. + /// The glyph run's width. + /// + IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width); } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index d9a68b236a..2fa249f101 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -445,7 +445,7 @@ namespace Avalonia.Rendering foreach (var r in _dirtyRectsDisplay) { var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); - context.FillRectangle(brush, r.Rect); + context.DrawRectangle(brush,null, r.Rect); } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index e341f02901..1e7b5c2923 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; @@ -19,7 +18,7 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface, + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), FontSize = s_fontSize }; } @@ -51,7 +50,7 @@ namespace Avalonia.Rendering var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); context.Transform = Matrix.Identity; - context.FillRectangle(Brushes.Black, rect); + context.DrawRectangle(Brushes.Black,null, rect); context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3af56f5215..a169a629be 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -152,13 +152,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY)) { - Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush))); + Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, CreateChildScene(brush))); } else { @@ -191,20 +191,20 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - var next = NextDrawAs(); + var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); } + else { ++_drawOperationindex; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) { throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs new file mode 100644 index 0000000000..b3c4fdbac0 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -0,0 +1,91 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; + +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a glyph run draw. + /// + internal class GlyphRunNode : BrushDrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The foreground brush. + /// The glyph run to draw. + /// The baseline origin of the glyph run. + /// Child scenes for drawing visual brushes. + public GlyphRunNode( + Matrix transform, + IBrush foreground, + GlyphRun glyphRun, + Point baselineOrigin, + IDictionary childScenes = null) + : base(glyphRun.Bounds, transform, null) + { + Transform = transform; + Foreground = foreground?.ToImmutable(); + GlyphRun = glyphRun; + BaselineOrigin = baselineOrigin; + ChildScenes = childScenes; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the foreground brush. + /// + public IBrush Foreground { get; } + + /// + /// Gets the glyph run to draw. + /// + public GlyphRun GlyphRun { get; } + + /// + /// Gets the baseline origin. + /// + public Point BaselineOrigin { get; set; } + + /// + public override IDictionary ChildScenes { get; } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The foreground of the other draw operation. + /// The glyph run of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + { + return transform == Transform && + Equals(foreground, Foreground) && + Equals(glyphRun, GlyphRun); + } + + /// + public override bool HitTest(Point p) => Bounds.Contains(p); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 0f3581b84c..b40afe78c5 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -21,14 +22,16 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The rectangle corner radius. + /// The radius in the Y dimension of the rounded corners. + /// The radius in the X dimension of the rounded corners. /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, IBrush brush, IPen pen, Rect rect, - float cornerRadius, + double radiusX, + double radiusY, IDictionary childScenes = null) : base(rect, transform, pen) { @@ -36,7 +39,8 @@ namespace Avalonia.Rendering.SceneGraph Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - CornerRadius = cornerRadius; + RadiusX = radiusX; + RadiusY = radiusY; ChildScenes = childScenes; } @@ -61,9 +65,14 @@ namespace Avalonia.Rendering.SceneGraph public Rect Rect { get; } /// - /// Gets the rectangle corner radius. + /// The radius in the X dimension of the rounded corners. /// - public float CornerRadius { get; } + public double RadiusX { get; } + + /// + /// The radius in the Y dimension of the rounded corners. + /// + public double RadiusY { get; } /// public override IDictionary ChildScenes { get; } @@ -75,19 +84,21 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The rectangle of the other draw operation. - /// The rectangle corner radius of the other draw operation. + /// + /// /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius) + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect == Rect && - cornerRadius == CornerRadius; + Equals(brush, Brush) && + Equals(Pen, pen) && + rect == Rect && + Math.Abs(radiusX - RadiusX) < double.Epsilon && + Math.Abs(radiusY - RadiusY) < double.Epsilon; } /// @@ -95,15 +106,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - if (Brush != null) - { - context.FillRectangle(Brush, Rect, CornerRadius); - } - - if (Pen != null) - { - context.DrawRectangle(Pen, Rect, CornerRadius); - } + context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY); } /// diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs new file mode 100644 index 0000000000..c54ccc8ef1 --- /dev/null +++ b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs @@ -0,0 +1,154 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Utility +{ + /// + /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. + /// + /// The type of elements in the slice. + public readonly struct ReadOnlySlice : IReadOnlyList + { + public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } + + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length) + { + Buffer = buffer; + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets a value that indicates whether this instance of is Empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// The buffer. + /// + public ReadOnlyMemory Buffer { get; } + + public T this[int index] => Buffer.Span[Start + index]; + + /// + /// Returns a span of the underlying buffer. + /// + /// The of the underlying buffer. + public ReadOnlySpan AsSpan() + { + return Buffer.Span.Slice(Start, Length); + } + + /// + /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. + /// + /// The start of the sub slice. + /// The length of the sub slice. + /// A that contains the specified number of elements from the specified start. + public ReadOnlySlice AsSlice(int start, int length) + { + if (start < 0 || start >= Length) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (Start + start > End) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start + start, length); + } + + /// + /// Returns a specified number of contiguous elements from the start of the slice. + /// + /// The number of elements to return. + /// A that contains the specified number of elements from the start of this slice. + public ReadOnlySlice Take(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start, length); + } + + /// + /// Bypasses a specified number of elements in the slice and then returns the remaining elements. + /// + /// The number of elements to skip before returning the remaining elements. + /// A that contains the elements that occur after the specified index in this slice. + public ReadOnlySlice Skip(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start + length, Length - length); + } + + /// + /// Returns an enumerator for the slice. + /// + public ImmutableReadOnlyListStructEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + int IReadOnlyCollection.Count => Length; + + T IReadOnlyList.this[int index] => this[index]; + + public static implicit operator ReadOnlySlice(T[] array) + { + return new ReadOnlySlice(array); + } + + public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) + { + return new ReadOnlySlice(memory); + } + } +} diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f4306d3929..465b3a65be 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; @@ -121,7 +120,7 @@ namespace Avalonia { var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = ValidateVisualChild; + visualChildren.Validate = visual => ValidateVisualChild(visual); visualChildren.CollectionChanged += VisualChildrenChanged; VisualChildren = visualChildren; } @@ -173,7 +172,22 @@ namespace Avalonia /// public bool IsEffectivelyVisible { - get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + get + { + IVisual node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } } /// @@ -552,7 +566,7 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var root = this.FindAncestorOfType(); var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 567b676b1e..8e3c7e0765 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -14,7 +14,7 @@ namespace Avalonia.VisualTree public static class VisualExtensions { /// - /// Calculates the distance from a visual's . + /// Calculates the distance from a visual's ancestor. /// /// The visual. /// The ancestor visual. @@ -30,13 +30,39 @@ namespace Avalonia.VisualTree while (visual != null && visual != ancestor) { - ++result; visual = visual.VisualParent; + + result++; } return visual != null ? result : -1; } + /// + /// Calculates the distance from a visual's root. + /// + /// The visual. + /// + /// The number of steps from the visual to the root. + /// + public static int CalculateDistanceFromRoot(IVisual visual) + { + Contract.Requires(visual != null); + + var result = 0; + + visual = visual?.VisualParent; + + while (visual != null) + { + visual = visual.VisualParent; + + result++; + } + + return result; + } + /// /// Tries to get the first common ancestor of two visuals. /// @@ -47,8 +73,53 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + void GoUpwards(ref IVisual node, int count) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + } + } + + // We want to find lowest node first, then make sure that both nodes are at the same height. + // By doing that we can sometimes find out that other node is our lowest common ancestor. + var firstHeight = CalculateDistanceFromRoot(visual); + var secondHeight = CalculateDistanceFromRoot(target); + + if (firstHeight > secondHeight) + { + GoUpwards(ref visual, firstHeight - secondHeight); + } + else + { + GoUpwards(ref target, secondHeight - firstHeight); + } + + if (visual == target) + { + return visual; + } + + while (visual != null && target != null) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + } + + return null; } /// @@ -69,6 +140,57 @@ namespace Avalonia.VisualTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// First ancestor of given type. + public static T FindAncestorOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + IVisual parent = includeSelf ? visual : visual.VisualParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.VisualParent; + } + + return null; + } + + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// First descendant of given type. + public static T FindDescendantOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + if (includeSelf && visual is T result) + { + return result; + } + + return FindDescendantOfTypeCore(visual); + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -249,6 +371,31 @@ namespace Avalonia.VisualTree .Select(x => x.Element); } + private static T FindDescendantOfTypeCore(IVisual visual) where T : class + { + var visualChildren = visual.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; + } + private class ZOrderElement : IComparable { public IVisual Element { get; set; } diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index cb9d3389e4..a431ffcc1a 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -79,7 +79,7 @@ namespace Avalonia.X11 atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE}) .ToArray(); XChangeProperty(_x11.Display, window, property, - target, 32, PropertyMode.Replace, atoms, atoms.Length); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 32460fed86..6ff5b96f12 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -351,10 +351,17 @@ namespace Avalonia.X11 { if (ActivateTransientChildIfNeeded()) return; - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown - : RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonDown, + 2 => RawPointerEventType.MiddleButtonDown, + 3 => RawPointerEventType.RightButtonDown, + 8 => RawPointerEventType.XButton1Down, + 9 => RawPointerEventType.XButton2Down + }, + ref ev, ev.ButtonEvent.state); else { var delta = ev.ButtonEvent.button == 4 @@ -372,10 +379,17 @@ namespace Avalonia.X11 } else if (ev.type == XEventName.ButtonRelease) { - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp - : RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonUp, + 2 => RawPointerEventType.MiddleButtonUp, + 3 => RawPointerEventType.RightButtonUp, + 8 => RawPointerEventType.XButton1Up, + 9 => RawPointerEventType.XButton2Up + }, + ref ev, ev.ButtonEvent.state); } else if (ev.type == XEventName.ConfigureNotify) { @@ -586,8 +600,12 @@ namespace Avalonia.X11 rv |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) rv |= RawInputModifiers.RightMouseButton; - if (state.HasFlag(XModifierMask.Button2Mask)) + if (state.HasFlag(XModifierMask.Button3Mask)) rv |= RawInputModifiers.MiddleMouseButton; + if (state.HasFlag(XModifierMask.Button4Mask)) + rv |= RawInputModifiers.XButton1MouseButton; + if (state.HasFlag(XModifierMask.Button5Mask)) + rv |= RawInputModifiers.XButton2MouseButton; if (state.HasFlag(XModifierMask.ShiftMask)) rv |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index e37ed39bee..ac14efe133 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -240,11 +240,15 @@ namespace Avalonia.X11 if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease) { var down = ev.Type == XiEventType.XI_ButtonPress; - var type = - ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) - : ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) - : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) - : (RawPointerEventType?)null; + var type = ev.Button switch + { + 1 => down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp, + 2 => down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp, + 3 => down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp, + 8 => down ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton1Up, + 9 => down ? RawPointerEventType.XButton2Down : RawPointerEventType.XButton2Up, + _ => (RawPointerEventType?)null + }; if (type.HasValue) client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); @@ -283,12 +287,14 @@ namespace Avalonia.X11 var buttons = ev->buttons.Mask; if (XIMaskIsSet(buttons, 1)) Modifiers |= RawInputModifiers.LeftMouseButton; - if (XIMaskIsSet(buttons, 2)) Modifiers |= RawInputModifiers.MiddleMouseButton; - if (XIMaskIsSet(buttons, 3)) Modifiers |= RawInputModifiers.RightMouseButton; + if (XIMaskIsSet(buttons, 8)) + Modifiers |= RawInputModifiers.XButton1MouseButton; + if (XIMaskIsSet(buttons, 9)) + Modifiers |= RawInputModifiers.XButton2MouseButton; } Valuators = new Dictionary(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a466714136..20f68df820 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. + anchor = context.GetFirstParent(); + } + // If a control was not found, then try to find the highest-level style as the XAML // file could be a XAML file containing only styles. return anchor ?? diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index dbe5800e55..b4545f792e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -26,6 +26,7 @@ namespace Avalonia.Data public Binding() { FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -60,6 +61,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -209,6 +215,7 @@ namespace Avalonia.Data observer, targetType, fallback, + TargetNullValue, converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); @@ -224,9 +231,9 @@ namespace Avalonia.Data { Contract.Requires(target != null); - if (!(target is IStyledElement)) + if (!(target is IDataContextProvider)) { - target = anchor as IStyledElement; + target = anchor as IDataContextProvider; if (target == null) { diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 29945e25c3..4325ad8a74 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -37,6 +37,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -57,6 +62,12 @@ namespace Avalonia.Data /// public string StringFormat { get; set; } + public MultiBinding() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -102,6 +113,11 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); + if (converted == null) + { + converted = TargetNullValue; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 47e651ce91..d06cfa69a7 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -184,37 +184,40 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { - using (var paint = CreatePaint(pen, rect.Size)) - { - var rc = rect.ToSKRect(); + var rc = rect.ToSKRect(); + var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; - if (Math.Abs(cornerRadius) < float.Epsilon) - { - Canvas.DrawRect(rc, paint.Paint); - } - else + if (brush != null) + { + using (var paint = CreatePaint(brush, rect.Size)) { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } } - } - /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) - { - using (var paint = CreatePaint(brush, rect.Size)) + if (pen?.Brush != null) { - var rc = rect.ToSKRect(); - - if (Math.Abs(cornerRadius) < float.Epsilon) - { - Canvas.DrawRect(rc, paint.Paint); - } - else + using (var paint = CreatePaint(pen, rect.Size)) { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } } } @@ -229,6 +232,20 @@ namespace Avalonia.Skia } } + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) + { + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + paint.ApplyTo(glyphRunImpl.Paint); + + Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, + (float)baselineOrigin.Y, glyphRunImpl.Paint); + } + } + /// public IRenderTargetBitmapImpl CreateLayer(Size size) { diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 03de82178a..727947e59d 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,9 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -13,13 +15,11 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - public FontManagerImpl() + public string GetDefaultFontFamilyName() { - DefaultFontFamilyName = SKTypeface.Default.FamilyName; + return SKTypeface.Default.FamilyName; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) @@ -30,53 +30,86 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; - } + [ThreadStatic] private static string[] t_languageTagBuffer; - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - var fontFamilyName = FontFamily.Default.Name; - if (culture == null) { culture = CultureInfo.CurrentUICulture; } + if (t_languageTagBuffer == null) + { + t_languageTagBuffer = new string[2]; + } + + t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; + t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; + if (fontFamily != null) { foreach (var familyName in fontFamily.FamilyNames) { var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontFamilyName = familyName; + fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); - break; + return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { - fontFamilyName = skTypeface.FamilyName; + fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + + return true; + } + } + + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + var skTypeface = SKTypeface.Default; + + if (typeface.FontFamily.Key == null) + { + foreach (var familyName in typeface.FontFamily.FamilyNames) + { + skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + if (skTypeface == SKTypeface.Default) + { + continue; + } + + break; } } + else + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + } - return GetTypeface(fontFamilyName, fontWeight, fontStyle); + return new GlyphTypefaceImpl(skTypeface); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a9358cb458..8effb94ca9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -29,7 +29,7 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; _paint = new SKPaint { @@ -38,7 +38,7 @@ namespace Avalonia.Skia IsAntialias = true, LcdRenderText = true, SubpixelText = true, - Typeface = entry.SKTypeface, + Typeface = glyphTypeface.Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() }; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs new file mode 100644 index 0000000000..e0f62d6085 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + public class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + { + Paint = paint; + TextBlob = textBlob; + } + + /// + /// Gets the paint to draw with. + /// + public SKPaint Paint { get; } + + /// + /// Gets the text blob to draw. + /// + public SKTextBlob TextBlob { get; } + + void IDisposable.Dispose() + { + TextBlob.Dispose(); + Paint.Dispose(); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index e46f766255..bb2650a5c6 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -14,9 +14,9 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface) { - Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface; + Typeface = typeface; Face = new Face(GetTable) { @@ -61,6 +61,8 @@ namespace Avalonia.Skia { StrikethroughThickness = strikethroughThickness; } + + IsFixedPitch = Typeface.IsFixedPitch; } public Face Face { get; } @@ -81,7 +83,6 @@ namespace Avalonia.Skia /// public int LineGap { get; } - //ToDo: Get these values from HarfBuzz /// public int UnderlinePosition { get; } @@ -94,6 +95,9 @@ namespace Avalonia.Skia /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ee0cfb2f06..05c3bbdaa0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.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.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -18,9 +17,6 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -60,7 +56,7 @@ namespace Avalonia.Skia Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); @@ -155,9 +151,95 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); + } + + /// + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var count = glyphRun.GlyphIndices.Length; + + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var typeface = glyphTypeface.Typeface; + + var paint = new SKPaint + { + TextSize = (float)glyphRun.FontRenderingEmSize, + Typeface = typeface, + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + using (var textBlobBuilder = new SKTextBlobBuilder()) + { + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + + if (glyphRun.GlyphOffsets.IsEmpty) + { + width = 0; + + var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); + + if (!glyphTypeface.IsFixedPitch) + { + var positions = buffer.GetPositionSpan(); + + for (var i = 0; i < count; i++) + { + positions[i] = (float)width; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + width += glyphRun.GlyphAdvances[i]; + } + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan()); + } + else + { + var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; + + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan()); + + width = currentX; + } + + var textBlob = textBlobBuilder.Build(); + + return new GlyphRunImpl(paint, textBlob); + } } } } diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6aabfae39 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")] diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 577567a8a1..d1c1961a8a 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -5,58 +5,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; +using Avalonia.Media.Fonts; +using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) + public void AddTypeface(FontKey key, SKTypeface typeface) { - if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) - { - fontFamily = new ConcurrentDictionary(); - - _fontFamilies.TryAdd(familyName, fontFamily); - } - - fontFamily.TryAdd(key, entry); + _typefaces.TryAdd(key, typeface); } - public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) + public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - var key = new FontKey(fontWeight, fontStyle); + var key = new FontKey(fontFamily, fontWeight, fontStyle); - return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? - fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : - new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default); + return GetNearestMatch(_typefaces, key); } - private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - var keys = fontFamily.Keys.Where( + if (typefaces.ContainsKey(key)) + { + return typefaces[key]; + } + + var keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } - key = keys.FirstOrDefault(); + if (keys.Length == 0) + { + return SKTypeface.Default; + } - fontFamily.TryGetValue(key, out var entry); + key = keys[0]; - return entry; + return typefaces[key]; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4bb42c7118..71edae26df 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,13 +45,11 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var skTypeface = SKTypeface.FromStream(assetStream); + var typeface = SKTypeface.FromStream(assetStream); - var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); - var entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); + typeFaceCollection.AddTypeface(key, typeface); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index ce3aef755b..f16e967f42 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,11 +25,6 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); - - var fontManager = new FontManagerImpl(); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs deleted file mode 100644 index 1c2b855032..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Concurrent; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - /// - /// Cache for Skia typefaces. - /// - internal static class TypefaceCache - { - private static readonly ConcurrentDictionary> s_cache = - new ConcurrentDictionary>(); - - public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - if (fontFamily.Key != null) - { - return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) - .Get(fontFamily.Name, fontWeight, fontStyle); - } - - var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - - var key = new FontKey(fontWeight, fontStyle); - - if (typefaceCollection.TryGetValue(key, out var entry)) - { - return entry; - } - - var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - - var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - - entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typefaceCollection[key] = entry; - - return entry; - } - } -} diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs deleted file mode 100644 index ef9f889819..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class TypefaceCollectionEntry - { - public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface) - { - Typeface = typeface; - SKTypeface = skTypeface; - } - public Typeface Typeface { get; } - public SKTypeface SKTypeface { get; } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1bda5157a5..a2bedf3190 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Platform; +using SharpDX.DirectWrite; +using GlyphRun = Avalonia.Media.GlyphRun; +using TextAlignment = Avalonia.Media.TextAlignment; namespace Avalonia { @@ -28,8 +31,6 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -109,7 +110,6 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -194,9 +194,57 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, data, size, dpi, stride); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); + } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var glyphCount = glyphRun.GlyphIndices.Length; + + var run = new SharpDX.DirectWrite.GlyphRun + { + FontFace = glyphTypeface.FontFace, + FontSize = (float)glyphRun.FontRenderingEmSize + }; + + var indices = new short[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + indices[i] = (short)glyphRun.GlyphIndices[i]; + } + + run.Indices = indices; + + run.Advances = new float[glyphCount]; + + width = 0; + + for (var i = 0; i < glyphCount; i++) + { + run.Advances[i] = (float)glyphRun.GlyphAdvances[i]; + width += run.Advances[i]; + } + + run.Offsets = new GlyphOffset[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + var offset = glyphRun.GlyphOffsets[i]; + + run.Offsets[i] = new GlyphOffset + { + AdvanceOffset = (float)offset.X, + AscenderOffset = (float)offset.Y + }; + } + + return new GlyphRunImpl(run); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index b455c4fbee..78bf25d607 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); + int index; - foreach (var familyName in fontFamily.FamilyNames) + foreach (var name in fontFamily.FamilyNames) { - if (fontCollection.FindFamilyName(familyName, out var index)) + if (fontCollection.FindFamilyName(name, out index)) { return fontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, @@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media } } - InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + InstalledFontCollection.FindFamilyName("Segoe UI", out index); - return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, FontStretch.Normal, (FontStyle)typeface.Style); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 39d801eb2f..aa13003643 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -230,34 +230,64 @@ namespace Avalonia.Direct2D1.Media } } - /// - /// Draws the outline of a rectangle. - /// - /// The pen. - /// The rectangle bounds. - /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius) + /// + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { - using (var brush = CreateBrush(pen.Brush, rect.Size)) - using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + var rc = rect.ToDirect2D(); + var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + + if (brush != null) { - if (brush.PlatformBrush != null) + using (var b = CreateBrush(brush, rect.Size)) { - if (cornerRadius == 0) + if (b.PlatformBrush != null) { - _deviceContext.DrawRectangle( - rect.ToDirect2D(), - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.FillRoundedRectangle( + new RoundedRectangle + { + Rect = new RawRectangleF( + (float)rect.X, + (float)rect.Y, + (float)rect.Right, + (float)rect.Bottom), + RadiusX = (float)radiusX, + RadiusY = (float)radiusY + }, + b.PlatformBrush); + } + else + { + _deviceContext.FillRectangle(rc, b.PlatformBrush); + } } - else + } + } + + if (pen?.Brush != null) + { + using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + { + if (wrapper.PlatformBrush != null) { - _deviceContext.DrawRoundedRectangle( - new RoundedRectangle { Rect = rect.ToDirect2D(), RadiusX = cornerRadius, RadiusY = cornerRadius }, - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.DrawRoundedRectangle( + new RoundedRectangle { Rect = rc, RadiusX = (float)radiusX, RadiusY = (float)radiusY }, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } + else + { + _deviceContext.DrawRectangle( + rc, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } } } } @@ -287,37 +317,18 @@ namespace Avalonia.Direct2D1.Media } /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius) + /// The foreground. + /// The glyph run. + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var b = CreateBrush(brush, rect.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) { - if (b.PlatformBrush != null) - { - if (cornerRadius == 0) - { - _deviceContext.FillRectangle(rect.ToDirect2D(), b.PlatformBrush); - } - else - { - _deviceContext.FillRoundedRectangle( - new RoundedRectangle - { - Rect = new RawRectangleF( - (float)rect.X, - (float)rect.Y, - (float)rect.Right, - (float)rect.Bottom), - RadiusX = cornerRadius, - RadiusY = cornerRadius - }, - b.PlatformBrush); - } - } + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 94de397652..31604ad15f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - public FontManagerImpl() + public string GetDefaultFontFamilyName() { //ToDo: Implement a real lookup of the system's default font. - DefaultFontFamilyName = "segoe ui"; + return "Segoe UI"; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - //ToDo: Implement caching. - return new Typeface(fontFamily, fontWeight, fontStyle); - } - - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) - { - var fontFamilyName = FontFamily.Default.Name; - var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; for (var i = 0; i < familyCount; i++) @@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media continue; } - fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - break; + fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + + return true; } - return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return new GlyphTypefaceImpl(typeface); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b1a177ad24..8e492a66ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -22,10 +22,16 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) - using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, - typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, - (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) + var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; + var familyName = font.FontFamily.FamilyNames.GetString(0); + using (var textFormat = new DWrite.TextFormat( + Direct2D1Platform.DirectWriteFactory, + familyName, + font.FontFamily.FontCollection, + (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, + DWrite.FontStretch.Normal, + (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs new file mode 100644 index 0000000000..0b06d5ef3e --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -0,0 +1,19 @@ +using Avalonia.Platform; + +namespace Avalonia.Direct2D1.Media +{ + internal class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) + { + GlyphRun = glyphRun; + } + + public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } + + public void Dispose() + { + GlyphRun?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index 32def01c39..dfc3b48eaa 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media { DWFont = Direct2D1FontCollectionCache.GetFont(typeface); - FontFace = new FontFace(DWFont); + FontFace = new FontFace(DWFont).QueryInterface(); Face = new Face(GetTable); @@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media { StrikethroughThickness = strikethroughThickness; } + + IsFixedPitch = FontFace.IsMonospacedFont; } private Blob GetTable(Face face, Tag tag) @@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media public SharpDX.DirectWrite.Font DWFont { get; } - public FontFace FontFace { get; } + public FontFace1 FontFace { get; } public Face Face { get; } @@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index a09d5c2d1c..26a8526c16 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs @@ -2,9 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Platform; using Avalonia.Direct2D1; [assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), typeof(Direct2DChecker))] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")] + diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 2c6425e26c..ed32382760 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1098,7 +1098,10 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); - + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RevokeDragDrop(IntPtr hwnd); + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] public static extern HRESULT OleInitialize(IntPtr val); diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index d454c797fa..c6e04a29b4 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -7,9 +7,9 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class OleContext + internal class OleContext { - private static OleContext fCurrent; + private static OleContext s_current; internal static OleContext Current { @@ -18,13 +18,12 @@ namespace Avalonia.Win32 if (!IsValidOleThread()) return null; - if (fCurrent == null) - fCurrent = new OleContext(); - return fCurrent; + if (s_current == null) + s_current = new OleContext(); + return s_current; } } - private OleContext() { UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero); @@ -43,9 +42,21 @@ namespace Avalonia.Win32 internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) { if (hwnd?.HandleDescriptor != "HWND" || target == null) + { return false; + } return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; } + + internal bool UnregisterDragDrop(IPlatformHandle hwnd) + { + if (hwnd?.HandleDescriptor != "HWND") + { + return false; + } + + return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK; + } } } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index b17e0d6c09..37d047689c 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -6,7 +6,7 @@ using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class OleDropTarget : IDropTarget + internal class OleDropTarget : IDropTarget { private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9d35342fb9..0f5db58dfe 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -253,6 +253,11 @@ namespace Avalonia.Win32 public void Dispose() { + if (_dropTarget != null) + { + OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget = null; + } if (_hwnd != IntPtr.Zero) { UnmanagedMethods.DestroyWindow(_hwnd); @@ -540,34 +545,42 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: - if(ShouldIgnoreTouchEmulatedMessage()) + case UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN: + if (ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN - ? RawPointerEventType.LeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, + UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, + UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down + }, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP: - if(ShouldIgnoreTouchEmulatedMessage()) + case UnmanagedMethods.WindowsMessage.WM_XBUTTONUP: + if (ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP - ? RawPointerEventType.LeftButtonUp - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP - ? RawPointerEventType.RightButtonUp - : RawPointerEventType.MiddleButtonUp, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, + UnmanagedMethods.WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, + UnmanagedMethods.WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, + UnmanagedMethods.WindowsMessage.WM_XBUTTONUP => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Up : RawPointerEventType.XButton2Up, + }, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; @@ -627,15 +640,19 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: + case UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN: e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN - ? RawPointerEventType.NonClientLeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.NonClientLeftButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down, + }, PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; case WindowsMessage.WM_TOUCH: @@ -778,6 +795,10 @@ namespace Avalonia.Win32 modifiers |= RawInputModifiers.RightMouseButton; if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; + if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON1)) + modifiers |= RawInputModifiers.XButton1MouseButton; + if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON2)) + modifiers |= RawInputModifiers.XButton2MouseButton; return modifiers; } @@ -1056,5 +1077,7 @@ namespace Avalonia.Win32 } } IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + + private static int HighWord(int param) => param >> 16; } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index d51f56f558..8eb8d1b5db 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); target.OnNext("foo"); @@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Null_Value_Should_Use_TargetNullValue() + { + var data = new Class1 { StringValue = "foo" }; + + var target = new BindingExpression( + ExpressionObserver.Create(data, o => o.StringValue), + typeof(string), + AvaloniaProperty.UnsetValue, + "bar", + DefaultValueConverter.Instance); + + object result = null; + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + data.StringValue = null; + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; diff --git a/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs new file mode 100644 index 0000000000..1c9ba4e6ae --- /dev/null +++ b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Avalonia.Controls; + +namespace Avalonia.Benchmarks +{ + internal class ControlHierarchyCreator + { + public static List CreateChildren(List controls, IPanel parent, int childCount, int innerCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + for (int j = 0; j < innerCount; ++j) + { + var child = new Button(); + + parent.Children.Add(child); + + controls.Add(child); + } + + if (iterations > 0) + { + CreateChildren(controls, control, childCount, innerCount, iterations - 1); + } + + controls.Add(control); + } + + return controls; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs new file mode 100644 index 0000000000..a320f07063 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs @@ -0,0 +1,59 @@ +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class BindingsBenchmark + { + [Benchmark] + public void TwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + } + + [Benchmark] + public void UpdateTwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + for (int i = 0; i < 60; i++) + { + instance.IntValue = i; + } + } + private class TestClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + + public static readonly StyledProperty BoundValueProperty = + AvaloniaProperty.Register(nameof(BoundValue)); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int BoundValue + { + get => GetValue(BoundValueProperty); + set => SetValue(BoundValueProperty, value); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index b0490d8a0f..d03d17b4d3 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using Avalonia.Controls; -using Avalonia.Layout; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -10,48 +9,34 @@ namespace Avalonia.Benchmarks.Layout [MemoryDiagnoser] public class Measure { - private TestRoot root; - private List controls = new List(); + private readonly TestRoot _root; + private readonly List _controls = new List(); public Measure() { var panel = new StackPanel(); - root = new TestRoot { Child = panel }; - controls.Add(panel); - CreateChildren(panel, 3, 5); - root.LayoutManager.ExecuteInitialLayoutPass(root); - } - - [Benchmark] - public void Remeasure_Half() - { - var random = new Random(1); - foreach (var control in controls) + _root = new TestRoot { - if (random.Next(2) == 0) - { - control.InvalidateMeasure(); - } - } + Child = panel, + Renderer = new NullRenderer() + }; - root.LayoutManager.ExecuteLayoutPass(); + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 5); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); } - private void CreateChildren(IPanel parent, int childCount, int iterations) + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public void Remeasure() { - for (var i = 0; i < childCount; ++i) + foreach (var control in _controls) { - var control = new StackPanel(); - parent.Children.Add(control); - - if (iterations > 0) - { - CreateChildren(control, childCount, iterations - 1); - } - - controls.Add(control); + control.InvalidateMeasure(); } + + _root.LayoutManager.ExecuteLayoutPass(); } } } diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs new file mode 100644 index 0000000000..7167eafc87 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Benchmarks +{ + internal class NullRenderer : IRenderer + { + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler SceneInvalidated; + + public void AddDirty(IVisual visual) + { + } + + public void Dispose() + { + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + + public void Paint(Rect rect) + { + } + + public void RecalculateChildren(IVisual visual) + { + } + + public void Resized(Size size) + { + } + + public void Start() + { + } + + public void Stop() + { + } + } +} diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs new file mode 100644 index 0000000000..fc2380d670 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Traversal +{ + [MemoryDiagnoser] + public class VisualTreeTraversal + { + private readonly TestRoot _root; + private readonly List _controls = new List(); + private readonly List _shuffledControls; + + public VisualTreeTraversal() + { + var panel = new StackPanel(); + _root = new TestRoot { Child = panel, Renderer = new NullRenderer()}; + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4); + + var random = new Random(1); + + _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); + } + + [Benchmark] + public void FindAncestorOfType_Linq() + { + foreach (Control control in _controls) + { + control.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + } + } + + [Benchmark] + public void FindAncestorOfType_Optimized() + { + foreach (Control control in _controls) + { + control.FindAncestorOfType(); + } + } + + [Benchmark] + public void FindCommonVisualAncestor() + { + foreach (IVisual first in _controls) + { + foreach (Control second in _shuffledControls) + { + first.FindCommonVisualAncestor(second); + } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index df2508a3ed..6e87a90ea4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; @@ -245,6 +246,23 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(false, item.IsSelected); } + [Fact] + public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection() + { + var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) + }; + + Prepare(target); + target.Scroll.Offset = new Vector(0, 1); + + items.RemoveRange(0, 11); + } + private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) { _mouse.Click(listBox, item, mouseButton); @@ -383,14 +401,26 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ScrollViewerTemplate() { return new FuncControlTemplate((parent, scope) => - new ScrollContentPresenter + new Panel { - Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), - [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - }.RegisterInNameScope(scope)); + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty], + } + } + }); } private void Prepare(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index e2eb628512..34e6b228d0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(50, target.Value); } - [Fact] - public void Properties_Should_Not_Accept_Nan_And_Inifinity() - { - var target = new TestRange(); - - Assert.Throws(() => target.Minimum = double.NaN); - Assert.Throws(() => target.Minimum = double.PositiveInfinity); - Assert.Throws(() => target.Minimum = double.NegativeInfinity); - Assert.Throws(() => target.Maximum = double.NaN); - Assert.Throws(() => target.Maximum = double.PositiveInfinity); - Assert.Throws(() => target.Maximum = double.NegativeInfinity); - Assert.Throws(() => target.Value = double.NaN); - Assert.Throws(() => target.Value = double.PositiveInfinity); - Assert.Throws(() => target.Value = double.NegativeInfinity); - } - [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index a91b7a0701..ed8a39d063 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; @@ -33,6 +34,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -77,6 +80,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -527,6 +532,8 @@ namespace Avalonia.Controls.UnitTests Items = data, }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -893,6 +900,37 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, GetItem(target, 0, 1, 0).Level); } + [Fact] + public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() + { + // Issue #2985 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var parent = tree[0]; + var node = parent.Children[1]; + + parent.Children.Remove(node); + parent.Children.Add(node); + + var item = target.ItemContainerGenerator.Index.ContainerFromItem(node); + ApplyTemplates(new[] { item }); + + // #2985 causes ArgumentException here. + node.Children.Add(new Node()); + } + [Fact] public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index d87014f646..0508edd92f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,8 +228,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var windowImpl = Mock.Of(x => x.Scaling == 1); - var target = new Window(windowImpl); + var target = new Window(); target.Show(); target.Close(); diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs new file mode 100644 index 0000000000..82471915f4 --- /dev/null +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Reflection; +using Avalonia.Direct2D1.Media; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Direct2D1.UnitTests.Media +{ + public class FontManagerImplTests + { + private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"; + + [Fact] + public void Should_Create_Typeface_From_Fallback() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, Arial"))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Create_Typeface_For_Unknown_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("Unknown"))); + + var font = glyphTypeface.DWFont; + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Load_Typeface_From_Resource() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + + [Fact] + public void Should_Load_Nearest_Matching_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index a683e5cfca..f063d59ca4 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests It.IsAny>())) .Returns(new FormattedTextMock("TEST")); + renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl()); + var streamGeometry = new Mock(); streamGeometry.Setup(x => x.Open()) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 7e053392c7..cd33fae6f3 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(42, target.Value); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + var source = new Source { Foo = null }; + + var binding = new Binding + { + Source = source, + Path = "Foo", + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + [Fact] public void Null_Path_Should_Bind_To_DataContext() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 78c538d99d..f3e8ff9248 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("fallback", target.Text); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + + var binding = new MultiBinding + { + Converter = new NullValueConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, + }, + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) @@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data return AvaloniaProperty.UnsetValue; } } + + private class NullValueConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } } } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index e6cd800529..97e234a55b 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -78,9 +78,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media { ctx.Clear(Colors.Transparent); ctx.PushOpacity(0.8); - ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100)); - ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100)); - ctx.FillRectangle(Brushes.Gold, new Rect(40, 0, 20, 100)); + ctx.DrawRectangle(Brushes.Chartreuse, null, new Rect(0, 0, 20, 100)); + ctx.DrawRectangle(Brushes.Crimson, null, new Rect(20, 0, 20, 100)); + ctx.DrawRectangle(Brushes.Gold,null, new Rect(40, 0, 20, 100)); ctx.PopOpacity(); } @@ -90,8 +90,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media { using (var ctx = rtb.CreateDrawingContext(null)) { - ctx.FillRectangle(Brushes.Blue, new Rect(0, 0, 100, 100)); - ctx.FillRectangle(Brushes.Pink, new Rect(0, 20, 100, 10)); + ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100)); + ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10)); var rc = new Rect(0, 0, 60, 60); ctx.DrawImage(bmp.PlatformImpl, 1, rc, rc); diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index bca34dd69d..73e63ae2ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontWeight, fontStyle), + FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs new file mode 100644 index 0000000000..927f98b32b --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using SkiaSharp; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class FontManagerImplTests + { + private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"; + + [Fact] + public void Should_Create_Typeface_From_Fallback() + { + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName()))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); + + Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); + + Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + } + + [Fact] + public void Should_Create_Typeface_For_Unknown_Font() + { + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("Unknown"))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); + + Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); + + Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + } + + [Fact] + public void Should_Load_Typeface_From_Resource() + { + using (AvaloniaLocator.EnterScope()) + { + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal("Noto Mono", skTypeface.FamilyName); + } + } + + [Fact] + public void Should_Load_Nearest_Matching_Font() + { + using (AvaloniaLocator.EnterScope()) + { + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic)); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal("Noto Mono", skTypeface.FamilyName); + } + } + } +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index c6eeb1ec0e..320dbfeec3 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -81,89 +81,12 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("TestLogical1 > TestLogical3", selector.ToString()); } - public abstract class TestLogical : ILogical, IStyleable + public abstract class TestLogical : Control { - public TestLogical() + public ILogical LogicalParent { - Classes = new Classes(); - } - - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - public event EventHandler AttachedToLogicalTree; - public event EventHandler DetachedFromLogicalTree; - - public Classes Classes { get; } - - public string Name { get; set; } - - public bool IsAttachedToLogicalTree { get; } - - public IAvaloniaReadOnlyList LogicalChildren { get; set; } - - public ILogical LogicalParent { get; set; } - - public Type StyleKey { get; } - - public ITemplatedControl TemplatedParent { get; } - - IObservable IStyleable.StyleDetach { get; } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - throw new NotImplementedException(); + get => Parent; + set => ((ISetLogicalParent)this).SetParent(value); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index 496998ecd9..fd25b17ba4 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -144,7 +144,7 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new[] { true, false }, result); } - public class Control1 : TestControlBase + public class Control1 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index aef539becd..940b2b18ef 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -111,89 +111,12 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("TestLogical1.foo TestLogical3", selector.ToString()); } - public abstract class TestLogical : ILogical, IStyleable + public abstract class TestLogical : Control { - public TestLogical() + public ILogical LogicalParent { - Classes = new Classes(); - } - - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - public event EventHandler AttachedToLogicalTree; - public event EventHandler DetachedFromLogicalTree; - - public Classes Classes { get; } - - public string Name { get; set; } - - public bool IsAttachedToLogicalTree { get; } - - public IAvaloniaReadOnlyList LogicalChildren { get; set; } - - public ILogical LogicalParent { get; set; } - - public Type StyleKey { get; } - - public ITemplatedControl TemplatedParent { get; } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - throw new NotImplementedException(); - } - - public void NotifyResourcesChanged(ResourcesChangedEventArgs e) - { - throw new NotImplementedException(); + get => Parent; + set => ((ISetLogicalParent)this).SetParent(value); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs index 1c8a3a406e..26734a494b 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Name.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls; using Moq; using Xunit; @@ -52,7 +53,7 @@ namespace Avalonia.Styling.UnitTests Assert.Equal("Control1#foo", target.ToString()); } - public class Control1 : TestControlBase + public class Control1 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs index 2f3e2b8f34..7c23cb4d2c 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs @@ -103,11 +103,11 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(typeof(Control1), target.TargetType); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs index 05f3544366..a254c73c03 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_OfType.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls; using Moq; using Xunit; @@ -44,11 +45,11 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs index 521c73ce27..e9bc93c957 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls; using Xunit; namespace Avalonia.Styling.UnitTests @@ -78,7 +79,7 @@ namespace Avalonia.Styling.UnitTests default(Selector).OfType().Class("foo"), default(Selector).OfType().Class("bar")); - Assert.Equal(typeof(TestControlBase), target.TargetType); + Assert.Equal(typeof(Control), target.TargetType); } [Fact] @@ -91,15 +92,15 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(null, target.TargetType); } - public class Control1 : TestControlBase + public class Control1 : Control { } - public class Control2 : TestControlBase + public class Control2 : Control { } - public class Control3 : TestControlBase + public class Control3 : Control { } } diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs deleted file mode 100644 index d9fabf6f5d..0000000000 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public class TestControlBase : IStyleable - { - public TestControlBase() - { - Classes = new Classes(); - SubscribeCheckObservable = new TestObservable(); - } - -#pragma warning disable CS0067 // Event not used - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; -#pragma warning restore CS0067 - - public string Name { get; set; } - - public virtual Classes Classes { get; set; } - - public Type StyleKey => GetType(); - - public TestObservable SubscribeCheckObservable { get; private set; } - - public ITemplatedControl TemplatedParent - { - get; - set; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs deleted file mode 100644 index e92ac36e8f..0000000000 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; - -namespace Avalonia.Styling.UnitTests -{ - public abstract class TestTemplatedControl : ITemplatedControl, IStyleable - { - public event EventHandler PropertyChanged; - public event EventHandler InheritablePropertyChanged; - - public abstract Classes Classes - { - get; - } - - public abstract string Name - { - get; - } - - public abstract Type StyleKey - { - get; - } - - public abstract ITemplatedControl TemplatedParent - { - get; - } - - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - IObservable IStyleable.StyleDetach { get; } - - public object GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public T GetValue(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, object value, BindingPriority priority) - { - throw new NotImplementedException(); - } - - public void SetValue(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public IDisposable Bind(AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) - { - throw new NotImplementedException(); - } - - public bool IsAnimating(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - - public bool IsSet(AvaloniaProperty property) - { - throw new NotImplementedException(); - } - } -} diff --git a/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf new file mode 100644 index 0000000000..3560a3a0c8 Binary files /dev/null and b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf differ diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 272b1fc489..b1d89037da 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -7,6 +7,9 @@ false latest + + + @@ -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