diff --git a/Avalonia.sln b/Avalonia.sln index e40ebae4d6..810ba5c12f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -204,6 +204,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1895,6 +1897,30 @@ Global {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1951,6 +1977,7 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm index 083adc927d..feb0643654 100644 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ b/native/Avalonia.Native/src/OSX/gl.mm @@ -1,6 +1,7 @@ #include "common.h" #include #include +#include "window.h" template char (&ArrayCounter(T (&a)[N]))[N]; #define ARRAY_COUNT(a) (sizeof(ArrayCounter(a))) @@ -181,12 +182,12 @@ extern IAvnGlFeature* GetGlFeature() class AvnGlRenderingSession : public ComSingleObject { - NSView* _view; - NSWindow* _window; + AvnView* _view; + AvnWindow* _window; NSOpenGLContext* _context; public: FORWARD_IUNKNOWN() - AvnGlRenderingSession(NSWindow*window, NSView* view, NSOpenGLContext* context) + AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context) { _context = context; _window = window; @@ -195,14 +196,12 @@ public: virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { - auto fsize = [_view convertSizeToBacking: [_view frame].size]; - ret->Width = (int)fsize.width; - ret->Height = (int)fsize.height; + *ret = [_view getPixelSize]; return S_OK; } virtual HRESULT GetScaling(double* ret) override { - *ret = [_window backingScaleFactor]; + *ret = [_window getScaling]; return S_OK; } @@ -234,8 +233,17 @@ public: auto f = GetFeature(); if(f == NULL) return E_FAIL; - if(![_view lockFocusIfCanDraw]) + + @try + { + if(![_view lockFocusIfCanDraw]) + return E_ABORT; + } + @catch(NSException* exception) + { return E_ABORT; + } + auto gl = _context; CGLLockContext([_context CGLContextObj]); diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 932bc56a2e..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; +-(AvnPixelSize) getPixelSize; @end @interface AvnWindow : NSWindow @@ -22,6 +23,7 @@ class WindowBaseImpl; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; -(void) applyMenu:(NSMenu *)menu; +-(double) getScaling; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index c54829d750..3acc5e365b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -195,7 +195,11 @@ public: { @autoreleasepool { - [Window close]; + if (Window != nullptr) + { + [Window close]; + } + return S_OK; } } @@ -291,7 +295,14 @@ public: { @autoreleasepool { - return [View lockFocusIfCanDraw] == YES; + @try + { + return [View lockFocusIfCanDraw] == YES; + } + @catch (NSException*) + { + return NO; + } } } @@ -719,15 +730,33 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; } -- (void)dealloc +- (void)onClosed { + @synchronized (self) + { + _parent = nullptr; + } } -- (void)onClosed +- (BOOL)lockFocusIfCanDraw +{ + @synchronized (self) + { + if(_parent == nullptr) + { + return NO; + } + } + + return [super lockFocusIfCanDraw]; +} + +-(AvnPixelSize) getPixelSize { - _parent = NULL; + return _lastPixelSize; } - (NSEvent*) lastMouseDownEvent @@ -742,6 +771,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setWantsLayer:YES]; _parent = parent; _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; return self; } @@ -783,6 +814,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self addTrackingArea:_area]; _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } @@ -812,7 +847,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)drawRect:(NSRect)dirtyRect { + if (_parent == nullptr) + { + return; + } + _parent->BaseEvents->RunRenderPriorityJobs(); + @synchronized (self) { if(_swRenderedFrame != NULL) { @@ -879,7 +920,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) viewDidChangeBackingProperties { + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + [super viewDidChangeBackingProperties]; } @@ -1161,6 +1207,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _closed; NSMenu* _menu; bool _isAppMenuApplied; + double _lastScaling; +} + +-(double) getScaling +{ + return _lastScaling; } +(void)closeAll @@ -1174,10 +1226,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -- (void)dealloc -{ -} - - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; @@ -1232,6 +1280,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; + _closed = false; + + _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } @@ -1247,6 +1301,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return true; } +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + _lastScaling = [self backingScaleFactor]; +} + - (void)windowWillClose:(NSNotification *)notification { _closed = true; @@ -1257,9 +1316,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self restoreParentWindow]; parent->BaseEvents->Closed(); [parent->View onClosed]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self setContentView: nil]; - }); } } @@ -1406,18 +1462,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } - -// TODO this breaks resizing. -/*- (void)windowDidResize:(NSNotification *)notification -{ - - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -}*/ @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index dd2f27116d..7b3b8465ce 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -191,6 +191,7 @@ partial class Build : NukeBuild RunCoreTest("./tests/Avalonia.Animation.UnitTests"); RunCoreTest("./tests/Avalonia.Base.UnitTests"); RunCoreTest("./tests/Avalonia.Controls.UnitTests"); + RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); RunCoreTest("./tests/Avalonia.Input.UnitTests"); RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); RunCoreTest("./tests/Avalonia.Layout.UnitTests"); diff --git a/readme.md b/readme.md index 512b35a454..97c6509362 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). -Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) +Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) Use these commands in the Package Manager console to install Avalonia manually: ``` diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 874560a294..cbe2c62890 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -32,7 +32,11 @@ - + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index b44fac27cb..9b8f8af765 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,45 +1,52 @@ - - Image - Displays an image - - - - No Stretch - - - - - Fill - - + + + Image + Displays an image + - - Uniform - - + + + + Bitmap + + None + Fill + Uniform + UniformToFill + + + - - UniformToFill - - - - - Window Icon as an Image - - - + + Drawing + + None + Fill + Uniform + UniformToFill + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 792b25963e..bbe89d1dfd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,40 +1,41 @@ -using System.IO; -using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Media.Imaging; +using Avalonia.Media; namespace ControlCatalog.Pages { public class ImagePage : UserControl { - private Image iconImage; + private readonly Image _bitmapImage; + private readonly Image _drawingImage; + public ImagePage() { - this.InitializeComponent(); + InitializeComponent(); + _bitmapImage = this.FindControl("bitmapImage"); + _drawingImage = this.FindControl("drawingImage"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - iconImage = this.Get("Icon"); } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + public void BitmapStretchChanged(object sender, SelectionChangedEventArgs e) + { + if (_bitmapImage != null) + { + var comboxBox = (ComboBox)sender; + _bitmapImage.Stretch = (Stretch)comboxBox.SelectedIndex; + } + } + + public void DrawingStretchChanged(object sender, SelectionChangedEventArgs e) { - base.OnAttachedToVisualTree(e); - if (iconImage.Source == null) + if (_drawingImage != null) { - var windowRoot = e.Root as Window; - if (windowRoot != null) - { - using (var stream = new MemoryStream()) - { - windowRoot.Icon.Save(stream); - stream.Seek(0, SeekOrigin.Begin); - iconImage.Source = new Bitmap(stream); - } - } + var comboxBox = (ComboBox)sender; + _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } } diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index 3eb2276c48..f263786ab7 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -39,7 +39,7 @@ namespace RenderDemo.Pages ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } - context.DrawImage(_bitmap, 1, + context.DrawImage(_bitmap, new Rect(0, 0, 200, 200), new Rect(0, 0, 200, 200)); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); diff --git a/scripts/avalonia-rename.ps1 b/scripts/avalonia-rename.ps1 deleted file mode 100644 index c77dffb55d..0000000000 --- a/scripts/avalonia-rename.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -function Get-NewDirectoryName { - param ([System.IO.DirectoryInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.Parent.FullName $name -} - -function Get-NewFileName { - param ([System.IO.FileInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.DirectoryName $name -} - -function Rename-Contents { - param ([System.IO.FileInfo] $file) - - $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib") - - if ($extensions.Contains($file.Extension)) { - $text = [IO.File]::ReadAllText($file.FullName) - $text = $text.Replace("github.com/perspex", "github.com/avaloniaui") - $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI") - $text = $text.Replace("perspex", "avalonia") - $text = $text.Replace("Perspex", "Avalonia") - $text = $text.Replace("PERSPEX", "AVALONIA") - [IO.File]::WriteAllText($file.FullName, $text) - } -} - -function Process-Files { - param ([System.IO.DirectoryInfo] $item) - - $dirs = Get-ChildItem -Path $item.FullName -Directory - $files = Get-ChildItem -Path $item.FullName -File - - foreach ($dir in $dirs) { - Process-Files $dir.FullName - } - - foreach ($file in $files) { - Rename-Contents $file - - $renamed = Get-NewFileName $file - - if ($file.FullName -ne $renamed) { - Write-Host git mv $file.FullName $renamed - & git mv $file.FullName $renamed - } - } - - $renamed = Get-NewDirectoryName $item - - if ($item.FullName -ne $renamed) { - Write-Host git mv $item.FullName $renamed - & git mv $item.FullName $renamed - } -} - -& git submodule deinit . -& git clean -xdf -Process-Files . diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index ca45fb8c4d..cc1ac8ded6 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -65,26 +65,30 @@ namespace Avalonia.Animation } } - /// - /// Reacts to a change in a value in - /// order to animate the change if a is set for the property. - /// - /// The event args. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { - if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; + if (_transitions is null || _previousTransitions is null || priority == BindingPriority.Animation) + return; // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). foreach (var transition in _transitions) { - if (transition.Property == e.Property) + if (transition.Property == property) { - if (_previousTransitions.TryGetValue(e.Property, out var dispose)) + if (_previousTransitions.TryGetValue(property, out var dispose)) dispose.Dispose(); - var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + var instance = transition.Apply( + this, + Clock ?? Avalonia.Animation.Clock.GlobalClock, + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault()); - _previousTransitions[e.Property] = instance; + _previousTransitions[property] = instance; return; } } diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index e9cd0686d8..9f57455639 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -63,7 +63,7 @@ namespace Avalonia.Animation public IterationType RepeatType => _type; /// - /// Gets a value that indicates whether the is set to loop. + /// Gets a value that indicates whether the is set to Infinite. /// public bool IsInfinite => _type == IterationType.Infinite; diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index fdb04b6dfc..d1df5fa5e3 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -18,12 +18,14 @@ namespace Avalonia /// The class that is registering the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. public AttachedProperty( string name, - Type ownerType, + Type ownerType, StyledPropertyMetadata metadata, - bool inherits = false) - : base(name, ownerType, metadata, inherits) + bool inherits = false, + Func validate = null) + : base(name, ownerType, metadata, inherits, validate) { } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0499907ab8..6a00feaf79 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; +using Avalonia.PropertyStore; using Avalonia.Threading; -using Avalonia.Utilities; namespace Avalonia { @@ -20,13 +18,13 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink { private IAvaloniaObject _inheritanceParent; - private List _directBindings; + private List _directBindings; private PropertyChangedEventHandler _inpcChanged; private EventHandler _propertyChanged; - private EventHandler _inheritablePropertyChanged; + private List _inheritanceChildren; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -57,15 +55,6 @@ namespace Avalonia remove { _inpcChanged -= value; } } - /// - /// Raised when an inheritable value changes on this object. - /// - event EventHandler IAvaloniaObject.InheritablePropertyChanged - { - add { _inheritablePropertyChanged += value; } - remove { _inheritablePropertyChanged -= value; } - } - /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -83,47 +72,27 @@ namespace Avalonia set { VerifyAccess(); + if (_inheritanceParent != value) { - if (_inheritanceParent != null) - { - _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; - } + var oldParent = _inheritanceParent; + var valuestore = _values; - var oldInheritanceParent = _inheritanceParent; + _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; - var valuestore = _values; foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) { - if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) + if (valuestore?.IsSet(property) == true) { - // if local value set there can be no change + // If local value set there can be no change. continue; } - // get the value as it would have been with the previous InheritanceParent - object oldValue; - if (oldInheritanceParent is AvaloniaObject aobj) - { - oldValue = aobj.GetValueOrDefaultUnchecked(property); - } - else - { - oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - } - object newValue = GetDefaultValue(property); - - if (!Equals(oldValue, newValue)) - { - RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); - } + property.RouteInheritanceParentChanged(this, oldParent); } - if (_inheritanceParent != null) - { - _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; - } + _inheritanceParent?.AddInheritanceChild(this); } } } @@ -166,10 +135,56 @@ namespace Avalonia /// The property. public void ClearValue(AvaloniaProperty property) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteClearValue(this); + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(AvaloniaProperty property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + switch (property) + { + case StyledPropertyBase styled: + ClearValue(styled); + break; + case DirectPropertyBase direct: + ClearValue(direct); + break; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(StyledPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + _values?.ClearLocalValue(property); + } + + /// + /// Clears a 's local value. + /// + /// The property. + public void ClearValue(DirectPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - SetValue(property, AvaloniaProperty.UnsetValue); + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + p.InvokeSetter(this, p.GetUnsetValue(GetType())); } /// @@ -210,21 +225,23 @@ namespace Avalonia /// The value. public object GetValue(AvaloniaProperty property) { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } + property = property ?? throw new ArgumentNullException(nameof(property)); + return property.RouteGetValue(this); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public T GetValue(StyledPropertyBase property) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - if (property.IsDirect) - { - return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); - } - else - { - return GetValueOrDefaultUnchecked(property); - } + return GetValueOrInheritedOrDefault(property); } /// @@ -233,14 +250,13 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - public T GetValue(AvaloniaProperty property) + public T GetValue(DirectPropertyBase property) { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - return (T)GetValue((AvaloniaProperty)property); + var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + return registered.InvokeGetter(this); } /// @@ -284,16 +300,43 @@ namespace Avalonia object value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteSetValue(this, value, priority); + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The property. + /// The value. + /// The priority of the value. + public void SetValue( + StyledPropertyBase property, + T value, + BindingPriority priority = BindingPriority.LocalValue) + { + property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - if (property.IsDirect) + LogPropertySet(property, value, priority); + + if (value is UnsetValueType) { - SetDirectValue(property, value); + if (priority == BindingPriority.LocalValue) + { + Values.ClearLocalValue(property); + } + else + { + throw new NotSupportedException( + "Cannot set property to Unset at non-local value priority."); + } } - else + else if (!(value is DoNothingType)) { - SetStyledValue(property, value, priority); + Values.SetValue(property, value, priority); } } @@ -303,69 +346,35 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - /// The priority of the value. - public void SetValue( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue) + public void SetValue(DirectPropertyBase property, T value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); - SetValue((AvaloniaProperty)property, value, priority); + LogPropertySet(property, value, BindingPriority.LocalValue); + SetDirectValueUnchecked(property, value); } /// /// Binds a to an observable. /// + /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// - public IDisposable Bind( - AvaloniaProperty property, - IObservable source, + public IDisposable Bind( + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - Contract.Requires(source != null); - + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); - if (property.IsDirect) - { - if (property.IsReadOnly) - { - throw new ArgumentException($"The property {property.Name} is readonly."); - } - - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority LocalValue", - property, - GetDescription(source)); - - if (_directBindings == null) - { - _directBindings = new List(); - } - - return new DirectBindingSubscription(this, property, source); - } - else - { - Logger.TryGet(LogEventLevel.Verbose)?.Log( - LogArea.Property, - this, - "Bound {Property} to {Binding} with priority {Priority}", - property, - GetDescription(source), - priority); - - return Values.AddBinding(property, source, priority); - } + return Values.AddBinding(property, source, priority); } /// @@ -374,42 +383,90 @@ namespace Avalonia /// The type of the property. /// The property. /// The observable. - /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue) + DirectPropertyBase property, + IObservable> source) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + VerifyAccess(); + + property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); + + if (property.IsReadOnly) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + Logger.TryGet(LogEventLevel.Verbose)?.Log( + LogArea.Property, + this, + "Bound {Property} to {Binding} with priority LocalValue", + property, + GetDescription(source)); + + _directBindings ??= new List(); - return Bind(property, source.Select(x => (object)x), priority); + return new DirectBindingSubscription(this, property, source); } /// - /// Forces the specified property to be revalidated. + /// Coerces the specified . /// + /// The type of the property. /// The property. - public void Revalidate(AvaloniaProperty property) + public void CoerceValue(StyledPropertyBase property) { - VerifyAccess(); - _values?.Revalidate(property); + _values?.CoerceValue(property); + } + + /// + void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) + { + _inheritanceChildren ??= new List(); + _inheritanceChildren.Add(child); } - internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) + /// + void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child) + { + _inheritanceChildren?.Remove(child); + } + + void IAvaloniaObject.InheritedPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + Optional newValue) + { + if (property.Inherits && (_values == null || !_values.IsSet(property))) + { + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); + } + } + + /// + Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { - oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - oldValue; - newValue = (newValue == AvaloniaProperty.UnsetValue) ? - GetDefaultValue(property) : - newValue; + return _propertyChanged?.GetInvocationList(); + } - if (!Equals(oldValue, newValue)) + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) + { + oldValue = oldValue.HasValue ? oldValue : GetInheritedOrDefault(property); + newValue = newValue.HasValue ? newValue : newValue.WithValue(GetInheritedOrDefault(property)); + + LogIfError(property, newValue); + + if (!EqualityComparer.Default.Equals(oldValue.Value, newValue.Value)) { - RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); + RaisePropertyChanged(property, oldValue, newValue, priority); Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, @@ -421,39 +478,59 @@ namespace Avalonia (BindingPriority)priority); } } - - internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) - { - LogIfError(property, notification); - UpdateDataValidation(property, notification); - } - /// - Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() - { - return _propertyChanged?.GetInvocationList(); - } + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } /// - /// Gets all priority values set on the object. + /// Called for each inherited property when the changes. /// - /// A collection of property/value tuples. - internal IDictionary GetSetValues() => Values?.GetSetValues(); + /// The type of the property value. + /// The property. + /// The old inheritance parent. + internal void InheritanceParentChanged( + StyledPropertyBase property, + IAvaloniaObject oldParent) + { + var oldValue = oldParent switch + { + AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), + null => property.GetDefaultValue(GetType()), + _ => oldParent.GetValue(property) + }; - /// - /// Forces revalidation of properties when a property value changes. - /// - /// The property to that affects validation. - /// The affected properties. - protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected) + var newValue = GetInheritedOrDefault(property); + + if (!EqualityComparer.Default.Equals(oldValue, newValue)) + { + RaisePropertyChanged(property, oldValue, newValue); + } + } + + internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { - property.Changed.Subscribe(e => + if (property.IsDirect) { - foreach (var p in affected) + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Local Value"); + } + else if (_values != null) + { + var result = _values.GetDiagnostic(property); + + if (result != null) { - e.Sender.Revalidate(p); + return result; } - }); + } + + return new AvaloniaPropertyValue( + property, + GetValue(property), + BindingPriority.Unset, + "Unset"); } /// @@ -477,18 +554,25 @@ namespace Avalonia /// enabled. /// /// The property. - /// The new validation status. - protected virtual void UpdateDataValidation( - AvaloniaProperty property, - BindingNotification status) + /// The new binding value for the property. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingValue value) { } /// /// Called when a avalonia property changes on the object. /// - /// The event arguments. - protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + /// The property whose value has changed. + /// The old value of the property. + /// The new value of the property. + /// The priority of the new value. + protected virtual void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { } @@ -499,40 +583,57 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected internal void RaisePropertyChanged( - AvaloniaProperty property, - object oldValue, - object newValue, + protected internal void RaisePropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - VerifyAccess(); + property = property ?? throw new ArgumentNullException(nameof(property)); - AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); + VerifyAccess(); property.Notifying?.Invoke(this, true); try { - OnPropertyChanged(e); - property.NotifyChanged(e); + AvaloniaPropertyChangedEventArgs e = null; + var hasChanged = property.HasChangedSubscriptions; + + if (hasChanged || _propertyChanged != null) + { + e = new AvaloniaPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority); + } + + OnPropertyChanged(property, oldValue, newValue, priority); + + if (hasChanged) + { + property.NotifyChanged(e); + } _propertyChanged?.Invoke(this, e); if (_inpcChanged != null) { - PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, e2); + var inpce = new PropertyChangedEventArgs(property.Name); + _inpcChanged(this, inpce); } - if (property.Inherits) + if (property.Inherits && _inheritanceChildren != null) { - _inheritablePropertyChanged?.Invoke(this, e); + foreach (var child in _inheritanceChildren) + { + child.InheritedPropertyChanged( + property, + oldValue, + newValue.ToOptional()); + } } } finally @@ -561,216 +662,103 @@ namespace Avalonia return false; } - DeferredSetter setter = Values.GetDirectDeferredSetter(property); - - return setter.SetAndNotify(this, property, ref field, value); + var old = field; + field = value; + RaisePropertyChanged(property, old, value); + return true; } - /// - /// Tries to cast a value to a type, taking into account that the value may be a - /// . - /// - /// The value. - /// The type. - /// The cast value, or a . - private static object CastOrDefault(object value, Type type) + private T GetInheritedOrDefault(StyledPropertyBase property) { - var notification = value as BindingNotification; - - if (notification == null) + if (property.Inherits && InheritanceParent is AvaloniaObject o) { - return TypeUtilities.ConvertImplicitOrDefault(value, type); + return o.GetValueOrInheritedOrDefault(property); } - else - { - if (notification.HasValue) - { - notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type)); - } - return notification; - } + return property.GetDefaultValue(GetType()); } - /// - /// Gets the default value for a property. - /// - /// The property. - /// The default value. - private object GetDefaultValue(AvaloniaProperty property) - { - if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnchecked(property); - return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); - } - - /// - /// Gets the value or default value for a property. - /// - /// The property. - /// The default value. - private object GetValueOrDefaultUnchecked(AvaloniaProperty property) + private T GetValueOrInheritedOrDefault(StyledPropertyBase property) { - var aobj = this; - var valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - if (property.Inherits) - { - while (aobj.InheritanceParent is AvaloniaObject parent) - { - aobj = parent; - valuestore = aobj._values; - if (valuestore != null) - { - var result = valuestore.GetValue(property); - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - } - } - } - return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - } + var o = this; + var inherits = property.Inherits; + var value = default(T); - /// - /// Sets the value of a direct property. - /// - /// The property. - /// The value. - private void SetDirectValue(AvaloniaProperty property, object value) - { - void Set() + while (o != null) { - var notification = value as BindingNotification; + var values = o._values; - if (notification != null) + if (values?.TryGetValue(property, out value) == true) { - LogIfError(property, notification); - value = notification.Value; + return value; } - if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) + if (!inherits) { - var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - var finalValue = value == AvaloniaProperty.UnsetValue ? - metadata.UnsetValue : value; - - LogPropertySet(property, value, BindingPriority.LocalValue); - - accessor.SetValue(this, finalValue); + break; } - if (notification != null) - { - UpdateDataValidation(property, notification); - } + o = o.InheritanceParent as AvaloniaObject; } - if (Dispatcher.UIThread.CheckAccess()) - { - Set(); - } - else - { - Dispatcher.UIThread.Post(Set); - } + return property.GetDefaultValue(GetType()); } /// - /// Sets the value of a styled property. + /// Sets the value of a direct property. /// /// The property. /// The value. - /// The priority of the value. - private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) + private void SetDirectValueUnchecked(DirectPropertyBase property, T value) { - var notification = value as BindingNotification; + var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); - // We currently accept BindingNotifications for non-direct properties but we just - // strip them to their underlying value. - if (notification != null) + if (value is UnsetValueType) { - if (!notification.HasValue) - { - return; - } - else - { - value = notification.Value; - } + p.InvokeSetter(this, p.GetUnsetValue(GetType())); } - - var originalValue = value; - - if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) + else if (!(value is DoNothingType)) { - throw new ArgumentException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); + p.InvokeSetter(this, value); } - - LogPropertySet(property, value, priority); - Values.AddValue(property, value, (int)priority); } /// - /// Given a direct property, returns a registered avalonia property that is equivalent or - /// throws if not found. + /// Sets the value of a direct property. /// /// The property. - /// The registered property. - private AvaloniaProperty GetRegistered(AvaloniaProperty property) + /// The value. + private void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) { - var direct = property as IDirectPropertyAccessor; - - if (direct == null) - { - throw new AvaloniaInternalException( - "AvaloniaObject.GetRegistered should only be called for direct properties"); - } + var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); - if (property.OwnerType.IsAssignableFrom(GetType())) + if (p == null) { - return property; + throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } - var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .FirstOrDefault(x => x == property); + LogIfError(property, value); - if (result == null) + switch (value.Type) { - throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); + case BindingValueType.UnsetValue: + case BindingValueType.BindingError: + var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType())); + property.InvokeSetter(this, fallback); + break; + case BindingValueType.DataValidationError: + property.InvokeSetter(this, value); + break; + case BindingValueType.Value: + case BindingValueType.BindingErrorWithFallback: + case BindingValueType.DataValidationErrorWithFallback: + property.InvokeSetter(this, value); + break; } - return result; - } - - /// - /// Called when a property is changed on the current . - /// - /// The event sender. - /// The event args. - /// - /// Checks for changes in an inherited property value. - /// - private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - Contract.Requires(e != null); - - if (e.Property.Inherits && !IsSet(e.Property)) + if (p.IsDataValidationEnabled) { - RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue); + UpdateDataValidation(property, value); } } @@ -779,7 +767,7 @@ namespace Avalonia /// /// The observable. /// The description. - private string GetDescription(IObservable o) + private string GetDescription(object o) { var description = o as IDescription; return description?.Description ?? o.ToString(); @@ -789,12 +777,12 @@ namespace Avalonia /// Logs a mesage if the notification represents a binding error. /// /// The property being bound. - /// The binding notification. - private void LogIfError(AvaloniaProperty property, BindingNotification notification) + /// The binding notification. + private void LogIfError(AvaloniaProperty property, BindingValue value) { - if (notification.ErrorType == BindingErrorType.Error) + if (value.HasError) { - if (notification.Error is AggregateException aggregate) + if (value.Error is AggregateException aggregate) { foreach (var inner in aggregate.InnerExceptions) { @@ -803,7 +791,7 @@ namespace Avalonia } else { - LogBindingError(property, notification.Error); + LogBindingError(property, value.Error); } } } @@ -814,7 +802,7 @@ namespace Avalonia /// The property. /// The new value. /// The priority. - private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) + private void LogPropertySet(AvaloniaProperty property, T value, BindingPriority priority) { Logger.TryGet(LogEventLevel.Verbose)?.Log( LogArea.Property, @@ -825,16 +813,16 @@ namespace Avalonia priority); } - private class DirectBindingSubscription : IObserver, IDisposable + private class DirectBindingSubscription : IObserver>, IDisposable { - readonly AvaloniaObject _owner; - readonly AvaloniaProperty _property; - IDisposable _subscription; + private readonly AvaloniaObject _owner; + private readonly DirectPropertyBase _property; + private readonly IDisposable _subscription; public DirectBindingSubscription( AvaloniaObject owner, - AvaloniaProperty property, - IObservable source) + DirectPropertyBase property, + IObservable> source) { _owner = owner; _property = property; @@ -850,11 +838,22 @@ namespace Avalonia public void OnCompleted() => Dispose(); public void OnError(Exception error) => Dispose(); - - public void OnNext(object value) + public void OnNext(BindingValue value) { - var castValue = CastOrDefault(value, _property.PropertyType); - _owner.SetDirectValue(_property, castValue); + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.SetDirectValueUnchecked(_property, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner; + var property = _property; + var newValue = value; + + Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); + } } } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index ad1cefd4ea..a4c7fa95a5 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -68,6 +68,51 @@ namespace Avalonia return new AvaloniaPropertyObservable(o, property); } + /// + /// Gets an observable for a . + /// + /// The object. + /// The property. + /// + /// An observable which fires immediately with the current value of the property on the + /// object and subsequently each time the property value changes. + /// + /// + /// The subscription to is created using a weak reference. + /// + public static IObservable> GetBindingObservable( + this IAvaloniaObject o, + AvaloniaProperty property) + { + Contract.Requires(o != null); + Contract.Requires(property != null); + + return new AvaloniaPropertyBindingObservable(o, property); + } + + /// + /// Gets an observable for a . + /// + /// The object. + /// The property type. + /// The property. + /// + /// An observable which fires immediately with the current value of the property on the + /// object and subsequently each time the property value changes. + /// + /// + /// The subscription to is created using a weak reference. + /// + public static IObservable> GetBindingObservable( + this IAvaloniaObject o, + AvaloniaProperty property) + { + Contract.Requires(o != null); + Contract.Requires(property != null); + + return new AvaloniaPropertyBindingObservable(o, property); + } + /// /// Gets an observable that listens for property changed events for an /// . @@ -80,7 +125,7 @@ namespace Avalonia /// for the specified property. /// public static IObservable GetPropertyChangedObservable( - this IAvaloniaObject o, + this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); @@ -134,6 +179,167 @@ namespace Avalonia o.GetObservable(property)); } + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this IAvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this IAvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return property.RouteBind(target, source, priority); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return property switch + { + StyledPropertyBase styled => target.Bind(styled, source, priority), + DirectPropertyBase direct => target.Bind(direct, source), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), + }; + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return target.Bind( + property, + source.ToBindingValue(), + priority); + } + + /// + /// Binds a to an observable. + /// + /// The object. + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public static IDisposable Bind( + this IAvaloniaObject target, + AvaloniaProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return target.Bind( + property, + source.ToBindingValue(), + priority); + } + /// /// Binds a property on an to an . /// @@ -153,16 +359,16 @@ namespace Avalonia IBinding binding, object anchor = null) { - Contract.Requires(target != null); - Contract.Requires(property != null); - Contract.Requires(binding != null); + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + binding = binding ?? throw new ArgumentNullException(nameof(binding)); var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata; var result = binding.Initiate( target, property, - anchor, + anchor, metadata?.EnableDataValidation ?? false); if (result != null) @@ -175,6 +381,125 @@ namespace Avalonia } } + /// + /// Clears a 's local value. + /// + /// The object. + /// The property. + public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteClearValue(target); + } + + /// + /// Clears a 's local value. + /// + /// The object. + /// The property. + public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + switch (property) + { + case StyledPropertyBase styled: + target.ClearValue(styled); + break; + case DirectPropertyBase direct: + target.ClearValue(direct); + break; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + + /// + /// Gets a value. + /// + /// The object. + /// The property. + /// The value. + public static object GetValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property.RouteGetValue(target); + } + + /// + /// Gets a value. + /// + /// The type of the property. + /// The object. + /// The property. + /// The value. + public static T GetValue(this IAvaloniaObject target, AvaloniaProperty property) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + return property switch + { + StyledPropertyBase styled => target.GetValue(styled), + DirectPropertyBase direct => target.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + } + + /// + /// Sets a value. + /// + /// The object. + /// The property. + /// The value. + /// The priority of the value. + public static void SetValue( + this IAvaloniaObject target, + AvaloniaProperty property, + object value, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + property.RouteSetValue(target, value, priority); + } + + /// + /// Sets a value. + /// + /// The type of the property. + /// The object. + /// The property. + /// The value. + /// The priority of the value. + public static void SetValue( + this IAvaloniaObject target, + AvaloniaProperty property, + T value, + BindingPriority priority = BindingPriority.LocalValue) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + property = property ?? throw new ArgumentNullException(nameof(property)); + + switch (property) + { + case StyledPropertyBase styled: + target.SetValue(styled, value, priority); + break; + case DirectPropertyBase direct: + target.SetValue(direct, value); + break; + default: + throw new NotSupportedException("Unsupported AvaloniaProperty type."); + } + } + /// /// Subscribes to a property changed notifications for changes that originate from a /// . diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index ac7d2c60af..b305a9aaa2 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reactive.Subjects; -using System.Reflection; using Avalonia.Data; using Avalonia.Utilities; @@ -14,7 +12,7 @@ namespace Avalonia /// /// Base class for avalonia properties. /// - public class AvaloniaProperty : IEquatable + public abstract class AvaloniaProperty : IEquatable { /// /// Represents an unset property value. @@ -183,6 +181,8 @@ namespace Avalonia /// internal int Id { get; } + internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false; + /// /// Provides access to a property's binding via the /// indexer. @@ -255,7 +255,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -267,7 +268,8 @@ namespace Avalonia TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null, + Func validate = null, + Func coerce = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -275,14 +277,15 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); var result = new StyledProperty( name, typeof(TOwner), metadata, inherits, + validate, notifying); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; @@ -298,24 +301,26 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits); + var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(typeof(TOwner), result); registry.RegisterAttached(typeof(THost), result); @@ -332,7 +337,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A validation function. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, @@ -340,17 +346,18 @@ namespace Avalonia TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - validate: Cast(validate), - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, ownerType, metadata, inherits); + var result = new AttachedProperty(name, ownerType, metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(ownerType, result); registry.RegisterAttached(typeof(THost), result); @@ -365,9 +372,7 @@ namespace Avalonia /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. - /// - /// The value to use when the property is set to - /// + /// The value to use when the property is cleared. /// The default binding mode for the property. /// /// Whether the property is interested in data validation. @@ -383,13 +388,18 @@ namespace Avalonia where TOwner : IAvaloniaObject { Contract.Requires(name != null); + Contract.Requires(getter != null); var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, - defaultBindingMode: defaultBindingMode, - enableDataValidation: enableDataValidation); + defaultBindingMode: defaultBindingMode); - var result = new DirectProperty(name, getter, setter, metadata); + var result = new DirectProperty( + name, + getter, + setter, + metadata, + enableDataValidation); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } @@ -483,6 +493,12 @@ namespace Avalonia /// internal bool HasNotifyInitializedObservers => _initialized.HasObservers; + /// + /// Notifies the observable. + /// + /// The object being initialized. + internal abstract void NotifyInitialized(IAvaloniaObject o); + /// /// Notifies the observable. /// @@ -501,6 +517,42 @@ namespace Avalonia _changed.OnNext(e); } + /// + /// Routes an untyped ClearValue call to a typed call. + /// + /// The object instance. + internal abstract void RouteClearValue(IAvaloniaObject o); + + /// + /// Routes an untyped GetValue call to a typed call. + /// + /// The object instance. + internal abstract object RouteGetValue(IAvaloniaObject o); + + /// + /// Routes an untyped SetValue call to a typed call. + /// + /// The object instance. + /// The value. + /// The priority. + internal abstract void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority); + + /// + /// Routes an untyped Bind call to a typed call. + /// + /// The object instance. + /// The binding source. + /// The priority. + internal abstract IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority); + + internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent); + /// /// Overrides the metadata for the property on the specified type. /// @@ -548,35 +600,22 @@ namespace Avalonia return result; } - currentType = currentType.GetTypeInfo().BaseType; + currentType = currentType.BaseType; } _metadataCache[type] = _defaultMetadata; return _defaultMetadata; } - - [DebuggerHidden] - private static Func Cast(Func f) - where TOwner : IAvaloniaObject - { - if (f != null) - { - return (o, v) => (o is TOwner) ? f((TOwner)o, v) : v; - } - else - { - return null; - } - } - - } + /// /// Class representing the . /// - public class UnsetValueType + public sealed class UnsetValueType { + internal UnsetValueType() { } + /// /// Returns the string representation of the . /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 6082367723..479d730e48 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -4,32 +4,20 @@ using System; using Avalonia.Data; +#nullable enable + namespace Avalonia { /// /// Provides information for a avalonia property change. /// - public class AvaloniaPropertyChangedEventArgs : EventArgs + public abstract class AvaloniaPropertyChangedEventArgs : EventArgs { - /// - /// Initializes a new instance of the class. - /// - /// The object that the property changed on. - /// The property that changed. - /// The old value of the property. - /// The new value of the property. - /// The priority of the binding that produced the value. public AvaloniaPropertyChangedEventArgs( - AvaloniaObject sender, - AvaloniaProperty property, - object oldValue, - object newValue, + IAvaloniaObject sender, BindingPriority priority) { Sender = sender; - Property = property; - OldValue = oldValue; - NewValue = newValue; Priority = priority; } @@ -37,7 +25,7 @@ namespace Avalonia /// Gets the that the property changed on. /// /// The sender object. - public AvaloniaObject Sender { get; private set; } + public IAvaloniaObject Sender { get; } /// /// Gets the property that changed. @@ -45,30 +33,36 @@ namespace Avalonia /// /// The property that changed. /// - public AvaloniaProperty Property { get; private set; } + public AvaloniaProperty Property => GetProperty(); /// /// Gets the old value of the property. /// /// - /// The old value of the property. + /// The old value of the property or if the + /// property previously had no value. /// - public object OldValue { get; private set; } + public object? OldValue => GetOldValue(); /// /// Gets the new value of the property. /// /// - /// The new value of the property. + /// The new value of the property or if the + /// property previously had no value. /// - public object NewValue { get; private set; } + public object? NewValue => GetNewValue(); /// /// Gets the priority of the binding that produced the value. /// /// - /// The priority of the binding that produced the value. + /// The priority of the new value. /// public BindingPriority Priority { get; private set; } + + protected abstract AvaloniaProperty GetProperty(); + protected abstract object? GetOldValue(); + protected abstract object? GetNewValue(); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs new file mode 100644 index 0000000000..d8ac3752b3 --- /dev/null +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -0,0 +1,67 @@ +// 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.Data; + +#nullable enable + +namespace Avalonia +{ + /// + /// Provides information for a avalonia property change. + /// + public class AvaloniaPropertyChangedEventArgs : AvaloniaPropertyChangedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The object that the property changed on. + /// The property that changed. + /// The old value of the property. + /// The new value of the property. + /// The priority of the binding that produced the value. + public AvaloniaPropertyChangedEventArgs( + IAvaloniaObject sender, + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + : base(sender, priority) + { + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + /// + /// Gets the property that changed. + /// + /// + /// The property that changed. + /// + public new AvaloniaProperty Property { get; } + + /// + /// Gets the old value of the property. + /// + /// + /// The old value of the property. + /// + public new Optional OldValue { get; private set; } + + /// + /// Gets the new value of the property. + /// + /// + /// The new value of the property. + /// + public new BindingValue NewValue { get; private set; } + + protected override AvaloniaProperty GetProperty() => Property; + + protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); + + protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); + } +} diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 01daeafc3a..14c8630599 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -20,10 +20,14 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attached = new Dictionary>(); + private readonly Dictionary> _direct = + new Dictionary>(); private readonly Dictionary> _registeredCache = new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); + private readonly Dictionary> _directCache = + new Dictionary>(); private readonly Dictionary> _initializedCache = new Dictionary>(); private readonly Dictionary> _inheritedCache = @@ -105,6 +109,37 @@ namespace Avalonia return result; } + /// + /// Gets all direct s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegisteredDirect(Type type) + { + Contract.Requires(type != null); + + if (_directCache.TryGetValue(type, out var result)) + { + return result; + } + + var t = type; + result = new List(); + + while (t != null) + { + if (_direct.TryGetValue(t, out var direct)) + { + result.AddRange(direct.Values); + } + + t = t.BaseType; + } + + _directCache.Add(type, result); + return result; + } + /// /// Gets all inherited s registered on a type. /// @@ -150,13 +185,29 @@ namespace Avalonia /// /// The object. /// A collection of definitions. - public IEnumerable GetRegistered(AvaloniaObject o) + public IEnumerable GetRegistered(IAvaloniaObject o) { Contract.Requires(o != null); return GetRegistered(o.GetType()); } + /// + /// Finds a direct property as registered on an object. + /// + /// The object. + /// The direct property. + /// + /// The registered property or null if no matching property found. + /// + public DirectPropertyBase GetRegisteredDirect( + IAvaloniaObject o, + DirectPropertyBase property) + { + return FindRegisteredDirect(o, property) ?? + throw new ArgumentException($"Property '{property.Name} not registered on '{o.GetType()}"); + } + /// /// Finds a registered property on a type by name. /// @@ -200,7 +251,7 @@ namespace Avalonia /// /// The property name contains a '.'. /// - public AvaloniaProperty FindRegistered(AvaloniaObject o, string name) + public AvaloniaProperty FindRegistered(IAvaloniaObject o, string name) { Contract.Requires(o != null); Contract.Requires(name != null); @@ -208,6 +259,34 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } + /// + /// Finds a direct property as registered on an object. + /// + /// The object. + /// The direct property. + /// + /// The registered property or null if no matching property found. + /// + public DirectPropertyBase FindRegisteredDirect( + IAvaloniaObject o, + DirectPropertyBase property) + { + if (property.Owner == o.GetType()) + { + return property; + } + + foreach (var p in GetRegisteredDirect(o.GetType())) + { + if (p == property) + { + return (DirectPropertyBase)p; + } + } + + return null; + } + /// /// Finds a registered property by Id. /// @@ -273,6 +352,22 @@ namespace Avalonia inner.Add(property.Id, property); } + if (property.IsDirect) + { + if (!_direct.TryGetValue(type, out inner)) + { + inner = new Dictionary(); + inner.Add(property.Id, property); + _direct.Add(type, inner); + } + else if (!inner.ContainsKey(property.Id)) + { + inner.Add(property.Id, property); + } + + _directCache.Clear(); + } + if (!_properties.ContainsKey(property.Id)) { _properties.Add(property.Id, property); @@ -326,18 +421,6 @@ namespace Avalonia var type = o.GetType(); - void Notify(AvaloniaProperty property, object value) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - if (!_initializedCache.TryGetValue(type, out var initializationData)) { var visited = new HashSet(); @@ -373,14 +456,7 @@ namespace Avalonia foreach (PropertyInitializationData data in initializationData) { - if (!data.Property.HasNotifyInitializedObservers) - { - continue; - } - - object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value; - - Notify(data.Property, value); + data.Property.NotifyInitialized(o); } } diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 0a223cf7ee..be58ff796d 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Data; +using Avalonia.Utilities; namespace Avalonia { @@ -9,7 +11,7 @@ namespace Avalonia /// A typed avalonia property. /// /// The value type of the property. - public class AvaloniaProperty : AvaloniaProperty + public abstract class AvaloniaProperty : AvaloniaProperty { /// /// Initializes a new instance of the class. @@ -40,5 +42,29 @@ namespace Avalonia : base(source, ownerType, metadata) { } + + protected BindingValue TryConvert(object value) + { + if (value == UnsetValue) + { + return BindingValue.Unset; + } + else if (value == BindingOperations.DoNothing) + { + return BindingValue.DoNothing; + } + + if (!TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + var error = new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + Name, + value, + value?.GetType().FullName ?? "(null)")); + return BindingValue.BindingError(error); + } + + return converted; + } } } diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs deleted file mode 100644 index 5fc515f299..0000000000 --- a/src/Avalonia.Base/BoxedValue.cs +++ /dev/null @@ -1,28 +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. - -namespace Avalonia -{ - /// - /// Represents boxed value of type . - /// - /// Type of stored value. - internal readonly struct BoxedValue - { - public BoxedValue(T value) - { - Boxed = value; - Typed = value; - } - - /// - /// Boxed value. - /// - public object Boxed { get; } - - /// - /// Typed value. - /// - public T Typed { get; } - } -} diff --git a/src/Avalonia.Base/Collections/Pooled/ClearMode.cs b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs new file mode 100644 index 0000000000..d78ac8feab --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs @@ -0,0 +1,40 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +namespace Avalonia.Collections.Pooled +{ + /// + /// This enum allows control over how data is treated when internal + /// arrays are returned to the ArrayPool. Be careful to understand + /// what each option does before using anything other than the default + /// of Auto. + /// + public enum ClearMode + { + /// + /// Auto has different behavior depending on the host project's target framework. + /// .NET Core 2.1: Reference types and value types that contain reference types are cleared + /// when the internal arrays are returned to the pool. Value types that do not contain reference + /// types are not cleared when returned to the pool. + /// .NET Standard 2.0: All user types are cleared before returning to the pool, in case they + /// contain reference types. + /// For .NET Standard, Auto and Always have the same behavior. + /// + Auto = 0, + /// + /// The Always setting has the effect of always clearing user types before returning to the pool. + /// This is the default behavior on .NET Standard.You might want to turn this on in a .NET Core project + /// if you were concerned about sensitive data stored in value types leaking to other pars of your application. + /// + Always = 1, + /// + /// Never will cause pooled collections to never clear user types before returning them to the pool. + /// You might want to use this setting in a .NET Standard project when you know that a particular collection stores + /// only value types and you want the performance benefit of not taking time to reset array items to their default value. + /// Be careful with this setting: if used for a collection that contains reference types, or value types that contain + /// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances + /// that are still being referenced by arrays sitting in the ArrayPool. + /// + Never = 2 + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs new file mode 100644 index 0000000000..2b15388a13 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class ICollectionDebugView + { + private readonly ICollection _collection; + + public ICollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + T[] items = new T[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs new file mode 100644 index 0000000000..9bc3609dc5 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -0,0 +1,21 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +using System; +using System.Collections.Generic; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Represents a read-only collection of pooled elements that can be accessed by index + /// + /// The type of elements in the read-only pooled list. + + public interface IReadOnlyPooledList : IReadOnlyList + { + /// + /// Gets a for the items currently in the collection. + /// + ReadOnlySpan Span { get; } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs new file mode 100644 index 0000000000..f0d6b292cc --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -0,0 +1,1531 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Implements a variable-size list that uses a pooled array to store the + /// elements. A PooledList has a capacity, which is the allocated length + /// of the internal array. As elements are added to a PooledList, the capacity + /// of the PooledList is automatically increased as required by reallocating the + /// internal array. + /// + /// + /// This class is based on the code for but it supports + /// and uses when allocating internal arrays. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [Serializable] + public class PooledList : IList, IReadOnlyPooledList, IList, IDisposable, IDeserializationCallback + { + // internal constant copied from Array.MaxArrayLength + private const int MaxArrayLength = 0x7FEFFFFF; + private const int DefaultCapacity = 4; + private static readonly T[] s_emptyArray = Array.Empty(); + + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _items; // Do not rename (binary serialization) + private int _size; // Do not rename (binary serialization) + private int _version; // Do not rename (binary serialization) + private readonly bool _clearOnFree; + + #region Constructors + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode, ArrayPool customPool) + { + _items = s_emptyArray; + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, bool sizeToCapacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool, bool sizeToCapacity) : this(capacity, ClearMode.Auto, customPool, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool) : this(capacity, clearMode, customPool, false) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) + { + if (capacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + if (capacity == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(capacity); + } + + if (sizeToCapacity) + { + _size = capacity; + if (clearMode != ClearMode.Never) + { + Array.Clear(_items, 0, _size); + } + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + int count = span.Length; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + span.CopyTo(_items); + _size = count; + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection) : this(collection, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode) : this(collection, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ArrayPool customPool) : this(collection, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + c.CopyTo(_items, 0); + _size = count; + } + break; + + default: + _size = 0; + _items = s_emptyArray; + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + Add(en.Current); + } + break; + } + } + + #endregion + + /// + /// Gets a for the items currently in the collection. + /// + public Span Span => _items.AsSpan(0, _size); + + /// + ReadOnlySpan IReadOnlyPooledList.Span => Span; + + /// + /// Gets and sets the capacity of this list. The capacity is the size of + /// the internal array used to hold items. When set, the internal + /// Memory of the list is reallocated to the given capacity. + /// Note that the return value for this property may be larger than the property was set to. + /// + public int Capacity + { + get => _items.Length; + set + { + if (value < _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); + } + + if (value != _items.Length) + { + if (value > 0) + { + var newItems = _pool.Rent(value); + if (_size > 0) + { + Array.Copy(_items, newItems, _size); + } + ReturnArray(); + _items = newItems; + } + else + { + ReturnArray(); + _size = 0; + } + } + } + } + + /// + /// Read-only property describing how many elements are in the List. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool IList.IsFixedSize => false; + + bool ICollection.IsReadOnly => false; + + bool IList.IsReadOnly => false; + + int ICollection.Count => _size; + + bool ICollection.IsSynchronized => false; + + // Synchronization root for this object. + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Gets or sets the element at the given index. + /// + public T this[int index] + { + get + { + // Following trick can reduce the range check by one + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + return _items[index]; + } + + set + { + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + _items[index] = value; + _version++; + } + } + + private static bool IsCompatibleObject(object value) + { + // Non-null values are fine. Only accept nulls if T is a class or Nullable. + // Note that default(T) is not equal to null for value types except when T is Nullable. + return ((value is T) || (value == null && default(T) == null)); + } + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + this[index] = (T)value; + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T)); + } + } + } + + /// + /// Adds the given object to the end of this list. The size of the list is + /// increased by one. If required, the capacity of the list is doubled + /// before adding the new element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + _version++; + int size = _size; + if ((uint)size < (uint)_items.Length) + { + _size = size + 1; + _items[size] = item; + } + else + { + AddWithResize(item); + } + } + + // Non-inline from List.Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = _size; + EnsureCapacity(size + 1); + _size = size + 1; + _items[size] = item; + } + + int IList.Add(object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Add((T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + + return Count - 1; + } + + /// + /// Adds the elements of the given collection to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(IEnumerable collection) + => InsertRange(_size, collection); + + /// + /// Adds the elements of the given array to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(T[] array) + => AddRange(array.AsSpan()); + + /// + /// Adds the elements of the given to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(ReadOnlySpan span) + { + var newSpan = InsertSpan(_size, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + /// The number of items to add. + public Span AddSpan(int count) + => InsertSpan(_size, count); + + public ReadOnlyCollection AsReadOnly() + => new ReadOnlyCollection(this); + + /// + /// Searches a section of the list for a given element using a binary search + /// algorithm. + /// + /// + /// Elements of the list are compared to the search value using + /// the given IComparer interface. If comparer is null, elements of + /// the list are compared to the search value using the IComparable + /// interface, which in that case must be implemented by all elements of the + /// list and the given search value. This method assumes that the given + /// section of the list is already sorted; if this is not the case, the + /// result will be incorrect. + /// + /// The method returns the index of the given value in the list. If the + /// list does not contain the given value, the method returns a negative + /// integer. The bitwise complement operator (~) can be applied to a + /// negative result to produce the index of the first element (if any) that + /// is larger than the given search value. This is also the index at which + /// the search value should be inserted into the list in order for the list + /// to remain sorted. + /// + public int BinarySearch(int index, int count, T item, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + return Array.BinarySearch(_items, index, count, item, comparer); + } + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item) + => BinarySearch(0, Count, item, null); + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item, IComparer comparer) + => BinarySearch(0, Count, item, comparer); + + /// + /// Clears the contents of the PooledList. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _version++; + int size = _size; + _size = 0; + + if (size > 0 && _clearOnFree) + { + // Clear the elements so that the gc can reclaim the references. + Array.Clear(_items, 0, _size); + } + } + + /// + /// Contains returns true if the specified element is in the List. + /// It does a linear, O(n) search. Equality is determined by calling + /// EqualityComparer{T}.Default.Equals. + /// + public bool Contains(T item) + { + // PERF: IndexOf calls Array.IndexOf, which internally + // calls EqualityComparer.Default.IndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.IndexOf. + + return _size != 0 && IndexOf(item) != -1; + } + + bool IList.Contains(object item) + { + if (IsCompatibleObject(item)) + { + return Contains((T)item); + } + return false; + } + + public PooledList ConvertAll(Func converter) + { + if (converter == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.converter); + } + + var list = new PooledList(_size); + for (int i = 0; i < _size; i++) + { + list._items[i] = converter(_items[i]); + } + list._size = _size; + return list; + } + + /// + /// Copies this list to the given span. + /// + public void CopyTo(Span span) + { + if (span.Length < Count) + throw new ArgumentException("Destination span is shorter than the list to be copied."); + + Span.CopyTo(span); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_items, 0, array, arrayIndex, _size); + } + + // Copies this List into array, which must be of a + // compatible array type. + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + try + { + // Array.Copy will check for NULL. + Array.Copy(_items, 0, array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Ensures that the capacity of this list is at least the given minimum + /// value. If the current capacity of the list is less than min, the + /// capacity is increased to twice the current capacity or to min, + /// whichever is larger. + /// + private void EnsureCapacity(int min) + { + if (_items.Length < min) + { + int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > MaxArrayLength) + newCapacity = MaxArrayLength; + if (newCapacity < min) + newCapacity = min; + Capacity = newCapacity; + } + } + + public bool Exists(Func match) + => FindIndex(match) != -1; + + public bool TryFind(Func match, out T result) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public PooledList FindAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + var list = new PooledList(); + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + list.Add(_items[i]); + } + } + return list; + } + + public int FindIndex(Func match) + => FindIndex(0, _size, match); + + public int FindIndex(int startIndex, Func match) + => FindIndex(startIndex, _size - startIndex, match); + + public int FindIndex(int startIndex, int count, Func match) + { + if ((uint)startIndex > (uint)_size) + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + + if (count < 0 || startIndex > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + if (match is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) + { + if (match(_items[i])) + return i; + } + return -1; + } + + public bool TryFindLast(Func match, out T result) + { + if (match is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = _size - 1; i >= 0; i--) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public int FindLastIndex(Func match) + => FindLastIndex(_size - 1, _size, match); + + public int FindLastIndex(int startIndex, Func match) + => FindLastIndex(startIndex, startIndex + 1, match); + + public int FindLastIndex(int startIndex, int count, Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + if (_size == 0) + { + // Special case for 0 length List + if (startIndex != -1) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + else + { + // Make sure we're not out of range + if ((uint)startIndex >= (uint)_size) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + + // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0. + if (count < 0 || startIndex - count + 1 < 0) + { + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + } + + int endIndex = startIndex - count; + for (int i = startIndex; i > endIndex; i--) + { + if (match(_items[i])) + { + return i; + } + } + return -1; + } + + public void ForEach(Action action) + { + if (action == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action); + } + + int version = _version; + for (int i = 0; i < _size; i++) + { + if (version != _version) + { + break; + } + action(_items[i]); + } + + if (version != _version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + /// + /// Returns an enumerator for this list with the given + /// permission for removal of elements. If modifications made to the list + /// while an enumeration is in progress, the MoveNext and + /// GetObject methods of the enumerator will throw an exception. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + /// + /// Equivalent to PooledList.Span.Slice(index, count). + /// + public Span GetRange(int index, int count) + { + if (index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size - index < count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + return Span.Slice(index, count); + } + + /// + /// Returns the index of the first occurrence of a given value in + /// this list. The list is searched forwards from beginning to end. + /// + public int IndexOf(T item) + => Array.IndexOf(_items, item, 0, _size); + + int IList.IndexOf(object item) + { + if (IsCompatibleObject(item)) + { + return IndexOf((T)item); + } + return -1; + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and ending at count number of elements. + /// + public int IndexOf(T item, int index) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return Array.IndexOf(_items, item, index, _size - index); + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and upto count number of elements. + /// + public int IndexOf(T item, int index, int count) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + if (count < 0 || index > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + return Array.IndexOf(_items, item, index, count); + } + + /// + /// Inserts an element into this list at a given index. The size of the list + /// is increased by one. If required, the capacity of the list is doubled + /// before inserting the new element. + /// + public void Insert(int index, T item) + { + // Note that insertions at the end are legal. + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); + } + + if (_size == _items.Length) + EnsureCapacity(_size + 1); + if (index < _size) + { + Array.Copy(_items, index, _items, index + 1, _size - index); + } + _items[index] = item; + _size++; + _version++; + } + + void IList.Insert(int index, object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Insert(index, (T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, IEnumerable collection) + { + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count > 0) + { + EnsureCapacity(_size + count); + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + // If we're inserting a List into itself, we want to be able to deal with that. + if (this == c) + { + // Copy first part of _items to insert location + Array.Copy(_items, 0, _items, index, index); + // Copy last part of _items back to inserted location + Array.Copy(_items, index + count, _items, index * 2, _size - index); + } + else + { + c.CopyTo(_items, index); + } + _size += count; + } + break; + + default: + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + { + Insert(index++, en.Current); + } + } + break; + } + + _version++; + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, ReadOnlySpan span) + { + var newSpan = InsertSpan(index, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, T[] array) + { + if (array is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + InsertRange(index, array.AsSpan()); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + public Span InsertSpan(int index, int count) + => InsertSpan(index, count, true); + + private Span InsertSpan(int index, int count, bool clearOutput) + { + EnsureCapacity(_size + count); + + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + _size += count; + _version++; + + var output = _items.AsSpan(index, count); + + if (clearOutput && _clearOnFree) + { + output.Clear(); + } + + return output; + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at the end + /// and ending at the first element in the list. + /// + public int LastIndexOf(T item) + { + if (_size == 0) + { // Special case for empty list + return -1; + } + else + { + return LastIndexOf(item, _size - 1, _size); + } + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and ending at the first element in the list. + /// + public int LastIndexOf(T item, int index) + { + if (index >= _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return LastIndexOf(item, index, index + 1); + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and upto count elements + /// + public int LastIndexOf(T item, int index, int count) + { + if (Count != 0 && index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (Count != 0 && count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size == 0) + { + // Special case for empty list + return -1; + } + + if (index >= _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + if (count > index + 1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + return Array.LastIndexOf(_items, item, index, count); + } + + // Removes the element at the given index. The size of the list is + // decreased by one. + public bool Remove(T item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + void IList.Remove(object item) + { + if (IsCompatibleObject(item)) + { + Remove((T)item); + } + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_items[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_items[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _items[freeIndex++] = _items[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + /// + /// Removes the element at the given index. The size of the list is + /// decreased by one. + /// + public void RemoveAt(int index) + { + if ((uint)index >= (uint)_size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + _size--; + if (index < _size) + { + Array.Copy(_items, index + 1, _items, index, _size - index); + } + _version++; + + if (_clearOnFree) + { + // Clear the removed element so that the gc can reclaim the reference. + _items[_size] = default; + } + } + + /// + /// Removes a range of elements from this list. + /// + public void RemoveRange(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 0) + { + _size -= count; + if (index < _size) + { + Array.Copy(_items, index + count, _items, index, _size - index); + } + + _version++; + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, _size, count); + } + } + } + + /// + /// Reverses the elements in this list. + /// + public void Reverse() + => Reverse(0, _size); + + /// + /// Reverses the elements in a range of this list. Following a call to this + /// method, an element in the range given by index and count + /// which was previously located at index i will now be located at + /// index index + (index + count - i - 1). + /// + public void Reverse(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Reverse(_items, index, count); + } + _version++; + } + + /// + /// Sorts the elements in this list. Uses the default comparer and + /// Array.Sort. + /// + public void Sort() + => Sort(0, Count, null); + + /// + /// Sorts the elements in this list. Uses Array.Sort with the + /// provided comparer. + /// + /// + public void Sort(IComparer comparer) + => Sort(0, Count, comparer); + + /// + /// Sorts the elements in a section of this list. The sort compares the + /// elements to each other using the given IComparer interface. If + /// comparer is null, the elements are compared to each other using + /// the IComparable interface, which in that case must be implemented by all + /// elements of the list. + /// + /// This method uses the Array.Sort method to sort the elements. + /// + public void Sort(int index, int count, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Sort(_items, index, count, comparer); + } + _version++; + } + + public void Sort(Func comparison) + { + if (comparison == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + } + + if (_size > 1) + { + // List uses ArraySortHelper here but since it's an internal class, + // we're creating an IComparer using the comparison function to avoid + // duplicating all that code. + Array.Sort(_items, 0, _size, new Comparer(comparison)); + } + _version++; + } + + /// + /// ToArray returns an array containing the contents of the List. + /// This requires copying the List, which is an O(n) operation. + /// + public T[] ToArray() + { + if (_size == 0) + { + return s_emptyArray; + } + + return Span.ToArray(); + } + + /// + /// Sets the capacity of this list to the size of the list. This method can + /// be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and + /// release all memory referenced by the list, execute the following + /// statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + /// + public void TrimExcess() + { + int threshold = (int)(_items.Length * 0.9); + if (_size < threshold) + { + Capacity = _size; + } + } + + public bool TrueForAll(Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = 0; i < _size; i++) + { + if (!match(_items[i])) + { + return false; + } + } + return true; + } + + private void ReturnArray() + { + if (_items.Length == 0) + return; + + try + { + // Clear the elements so that the gc can reclaim the references. + _pool.Return(_items, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + + _items = s_emptyArray; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + /// + /// Returns the internal buffers to the ArrayPool. + /// + public void Dispose() + { + ReturnArray(); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledLists will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledList _list; + private int _index; + private readonly int _version; + private T _current; + + internal Enumerator(PooledList list) + { + _list = list; + _index = 0; + _version = list._version; + _current = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + var localList = _list; + + if (_version == localList._version && ((uint)_index < (uint)localList._size)) + { + _current = localList._items[_index]; + _index++; + return true; + } + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = _list._size + 1; + _current = default; + return false; + } + + public T Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _list._size + 1) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + return Current; + } + } + + void IEnumerator.Reset() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = default; + } + } + + private readonly struct Comparer : IComparer + { + private readonly Func _comparison; + + public Comparer(Func comparison) + { + _comparison = comparison; + } + + public int Compare(T x, T y) => _comparison(x, y); + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledStack.cs b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs new file mode 100644 index 0000000000..104a7f97e9 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs @@ -0,0 +1,699 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*============================================================================= +** +** +** Purpose: An array implementation of a generic stack. +** +** +=============================================================================*/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// A simple stack of objects. Internally it is implemented as an array, + /// so Push can be O(n). Pop is O(1). + /// + [DebuggerTypeProxy(typeof(StackDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledStack : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback + { + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _array; // Storage for stack elements. Do not rename (binary serialization) + private int _size; // Number of items in the stack. Do not rename (binary serialization) + private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization) + private readonly bool _clearOnFree; + + private const int DefaultCapacity = 4; + + #region Constructors + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Create a stack with the default initial capacity and a custom ArrayPool. + /// + public PooledStack(ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _array = Array.Empty(); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode, ArrayPool customPool) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + _pool = customPool ?? ArrayPool.Shared; + _array = _pool.Rent(capacity); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (enumerable) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + break; + + case ICollection collection: + if (collection.Count == 0) + { + _array = Array.Empty(); + } + else + { + _array = _pool.Rent(collection.Count); + collection.CopyTo(_array, 0); + _size = collection.Count; + } + break; + + default: + using (var list = new PooledList(enumerable)) + { + _array = _pool.Rent(list.Count); + list.Span.CopyTo(_array); + _size = list.Count; + } + break; + } + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + _array = _pool.Rent(span.Length); + span.CopyTo(_array); + _size = span.Length; + } + + #endregion + + /// + /// The number of items in the stack. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Removes all Objects from the Stack. + /// + public void Clear() + { + if (_clearOnFree) + { + Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references. + } + _size = 0; + _version++; + } + + /// + /// Compares items using the default equality comparer + /// + public bool Contains(T item) + { + // PERF: Internally Array.LastIndexOf calls + // EqualityComparer.Default.LastIndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.LastIndexOf. + + return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1; + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveWhere(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_array[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_array[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _array[freeIndex++] = _array[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_array, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + // Copies the stack into an array. + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + Debug.Assert(array != _array); + int srcIndex = 0; + int dstIndex = arrayIndex + _size; + while (srcIndex < _size) + { + array[--dstIndex] = _array[srcIndex++]; + } + } + + public void CopyTo(Span span) + { + if (span.Length < _size) + { + ThrowHelper.ThrowArgumentException_DestinationTooShort(); + } + + int srcIndex = 0; + int dstIndex = _size; + while (srcIndex < _size) + { + span[--dstIndex] = _array[srcIndex++]; + } + } + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (array.Rank != 1) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + if (array.GetLowerBound(0) != 0) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + try + { + Array.Copy(_array, 0, array, arrayIndex, _size); + Array.Reverse(array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Returns an IEnumerator for this PooledStack. + /// + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + public void TrimExcess() + { + if (_size == 0) + { + ReturnArray(replaceWith: Array.Empty()); + _version++; + return; + } + + int threshold = (int)(_array.Length * 0.9); + if (_size < threshold) + { + var newArray = _pool.Rent(_size); + if (newArray.Length < _array.Length) + { + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _version++; + } + else + { + // The array from the pool wasn't any smaller than the one we already had, + // (we can only control minimum size) so return it and do nothing. + // If we create an exact-sized array not from the pool, we'll + // get an exception when returning it to the pool. + _pool.Return(newArray); + } + } + } + + /// + /// Returns the top object on the stack without removing it. If the stack + /// is empty, Peek throws an InvalidOperationException. + /// + public T Peek() + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + return array[size]; + } + + public bool TryPeek(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + result = array[size]; + return true; + } + + /// + /// Pops an item from the top of the stack. If the stack is empty, Pop + /// throws an InvalidOperationException. + /// + public T Pop() + { + int size = _size - 1; + T[] array = _array; + + // if (_size == 0) is equivalent to if (size == -1), and this case + // is covered with (uint)size, thus allowing bounds check elimination + // https://github.com/dotnet/coreclr/pull/9773 + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + _version++; + _size = size; + T item = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return item; + } + + public bool TryPop(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + _version++; + _size = size; + result = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return true; + } + + /// + /// Pushes an item to the top of the stack. + /// + public void Push(T item) + { + int size = _size; + T[] array = _array; + + if ((uint)size < (uint)array.Length) + { + array[size] = item; + _version++; + _size = size + 1; + } + else + { + PushWithResize(item); + } + } + + // Non-inline from Stack.Push to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void PushWithResize(T item) + { + var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _array[_size] = item; + _version++; + _size++; + } + + /// + /// Copies the Stack to an array, in the same order Pop would return the items. + /// + public T[] ToArray() + { + if (_size == 0) + return Array.Empty(); + + T[] objArray = new T[_size]; + int i = 0; + while (i < _size) + { + objArray[i] = _array[_size - i - 1]; + i++; + } + return objArray; + } + + private void ThrowForEmptyStack() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Stack was empty."); + } + + private void ReturnArray(T[] replaceWith = null) + { + if (_array?.Length > 0) + { + try + { + _pool.Return(_array, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (!(replaceWith is null)) + { + _array = replaceWith; + } + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + public void Dispose() + { + ReturnArray(replaceWith: Array.Empty()); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledStacks will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledStack _stack; + private readonly int _version; + private int _index; + private T _currentElement; + + internal Enumerator(PooledStack stack) + { + _stack = stack; + _version = stack._version; + _index = -2; + _currentElement = default; + } + + public void Dispose() + { + _index = -1; + } + + public bool MoveNext() + { + bool retval; + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + if (_index == -2) + { // First call to enumerator. + _index = _stack._size - 1; + retval = (_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + return retval; + } + if (_index == -1) + { // End of enumeration. + return false; + } + + retval = (--_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + else + _currentElement = default; + return retval; + } + + public T Current + { + get + { + if (_index < 0) + ThrowEnumerationNotStartedOrEnded(); + return _currentElement; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == -1 || _index == -2); + throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended."); + } + + object IEnumerator.Current + { + get { return Current; } + } + + void IEnumerator.Reset() + { + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + _index = -2; + _currentElement = default; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs new file mode 100644 index 0000000000..b042388079 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class StackDebugView + { + private readonly PooledStack _stack; + + public StackDebugView(PooledStack stack) + { + _stack = stack ?? throw new ArgumentNullException(nameof(stack)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _stack.ToArray(); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs new file mode 100644 index 0000000000..74558229c3 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs @@ -0,0 +1,691 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +// This file defines an internal class used to throw exceptions in BCL code. +// The main purpose is to reduce code size. +// +// The old way to throw an exception generates quite a lot IL code and assembly code. +// Following is an example: +// C# source +// throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key); +// IL code: +// IL_0003: ldstr "key" +// IL_0008: ldstr "ArgumentNull_Key" +// IL_000d: call string System.Environment::GetResourceString(string) +// IL_0012: newobj instance void System.ArgumentNullException::.ctor(string,string) +// IL_0017: throw +// which is 21bytes in IL. +// +// So we want to get rid of the ldstr and call to Environment.GetResource in IL. +// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the +// argument name and resource name in a small integer. The source code will be changed to +// ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key); +// +// The IL code will be 7 bytes. +// IL_0008: ldc.i4.4 +// IL_0009: ldc.i4.4 +// IL_000a: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument) +// IL_000f: ldarg.0 +// +// This will also reduce the Jitted code size a lot. +// +// It is very important we do this for generic classes because we can easily generate the same code +// multiple times for different instantiation. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Avalonia.Collections.Pooled +{ + internal static class ThrowHelper + { + internal static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException(); + } + + internal static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + + internal static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException(); + } + + internal static void ThrowArgumentException_DestinationTooShort() + { + throw new ArgumentException("Destination too short."); + } + + internal static void ThrowArgumentException_OverlapAlignmentMismatch() + { + throw new ArgumentException("Overlap alignment mismatch."); + } + + internal static void ThrowArgumentOutOfRange_IndexException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.value, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.length, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_Count); + } + + internal static void ThrowWrongKeyTypeArgumentException(T key, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongKeyTypeArgumentException((object)key, targetType); + } + + internal static void ThrowWrongValueTypeArgumentException(T value, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongValueTypeArgumentException((object)value, targetType); + } + + private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key) + { + return new ArgumentException($"Error adding duplicate with key: {key}."); + } + + internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetAddingDuplicateWithKeyArgumentException((object)key); + } + + internal static void ThrowKeyNotFoundException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetKeyNotFoundException((object)key); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw GetArgumentException(resource); + } + + internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + throw GetArgumentException(resource, argument); + } + + private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + internal static void ThrowArgumentNullException(ExceptionResource resource) + { + throw new ArgumentNullException(GetResourceString(resource)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource) + { + throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, resource); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, paramNumber, resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw GetInvalidOperationException(resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e) + { + throw new InvalidOperationException(GetResourceString(resource), e); + } + + internal static void ThrowSerializationException(ExceptionResource resource) + { + throw new SerializationException(GetResourceString(resource)); + } + + internal static void ThrowSecurityException(ExceptionResource resource) + { + throw new System.Security.SecurityException(GetResourceString(resource)); + } + + internal static void ThrowRankException(ExceptionResource resource) + { + throw new RankException(GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException(ExceptionResource resource) + { + throw new NotSupportedException(GetResourceString(resource)); + } + + internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) + { + throw new UnauthorizedAccessException(GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource) + { + throw new ObjectDisposedException(objectName, GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(ExceptionResource resource) + { + throw new ObjectDisposedException(null, GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException() + { + throw new NotSupportedException(); + } + + internal static void ThrowAggregateException(List exceptions) + { + throw new AggregateException(exceptions); + } + + internal static void ThrowOutOfMemoryException() + { + throw new OutOfMemoryException(); + } + + internal static void ThrowArgumentException_Argument_InvalidArrayType() + { + throw new ArgumentException("Invalid array type."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() + { + throw new InvalidOperationException("Enumeration has not started."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded() + { + throw new InvalidOperationException("Enumeration has ended."); + } + + internal static void ThrowInvalidOperationException_EnumCurrent(int index) + { + throw GetInvalidOperationException_EnumCurrent(index); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() + { + throw new InvalidOperationException("Collection was modified during enumeration."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen() + { + throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_NoValue() + { + throw new InvalidOperationException("No value provided."); + } + + internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() + { + throw new InvalidOperationException("Concurrent operations are not supported."); + } + + internal static void ThrowInvalidOperationException_HandleIsNotInitialized() + { + throw new InvalidOperationException("Handle is not initialized."); + } + + internal static void ThrowFormatException_BadFormatSpecifier() + { + throw new FormatException("Bad format specifier."); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource) + { + return new InvalidOperationException(GetResourceString(resource)); + } + + private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType) + { + return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key)); + } + + private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType) + { + return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value)); + } + + private static KeyNotFoundException GetKeyNotFoundException(object key) + { + return new KeyNotFoundException($"Key not found: {key}"); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource)); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + return new ArgumentException(GetResourceString(resource), GetArgumentName(argument)); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index) + { + return new InvalidOperationException( + index < 0 ? + "Enumeration has not started" : + "Enumeration has ended"); + } + + // Allow nulls for reference types and Nullable, but not for value types. + // Aggressively inline so the jit evaluates the if in place and either drops the call altogether + // Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IfNullAndNullsAreIllegalThenThrow(object value, ExceptionArgument argName) + { + // Note that default(T) is not equal to null for value types except when T is Nullable. + if (!(default(T) == null) && value == null) + ThrowHelper.ThrowArgumentNullException(argName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowForUnsupportedVectorBaseType() where T : struct + { + if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) && + typeof(T) != typeof(short) && typeof(T) != typeof(ushort) && + typeof(T) != typeof(int) && typeof(T) != typeof(uint) && + typeof(T) != typeof(long) && typeof(T) != typeof(ulong) && + typeof(T) != typeof(float) && typeof(T) != typeof(double)) + { + ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionArgument enum value to the argument name string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } +#endif + + private static string GetArgumentName(ExceptionArgument argument) + { + switch (argument) + { + case ExceptionArgument.obj: + return "obj"; + case ExceptionArgument.dictionary: + return "dictionary"; + case ExceptionArgument.array: + return "array"; + case ExceptionArgument.info: + return "info"; + case ExceptionArgument.key: + return "key"; + case ExceptionArgument.text: + return "text"; + case ExceptionArgument.values: + return "values"; + case ExceptionArgument.value: + return "value"; + case ExceptionArgument.startIndex: + return "startIndex"; + case ExceptionArgument.task: + return "task"; + case ExceptionArgument.ch: + return "ch"; + case ExceptionArgument.s: + return "s"; + case ExceptionArgument.input: + return "input"; + case ExceptionArgument.list: + return "list"; + case ExceptionArgument.index: + return "index"; + case ExceptionArgument.capacity: + return "capacity"; + case ExceptionArgument.collection: + return "collection"; + case ExceptionArgument.item: + return "item"; + case ExceptionArgument.converter: + return "converter"; + case ExceptionArgument.match: + return "match"; + case ExceptionArgument.count: + return "count"; + case ExceptionArgument.action: + return "action"; + case ExceptionArgument.comparison: + return "comparison"; + case ExceptionArgument.exceptions: + return "exceptions"; + case ExceptionArgument.exception: + return "exception"; + case ExceptionArgument.enumerable: + return "enumerable"; + case ExceptionArgument.start: + return "start"; + case ExceptionArgument.format: + return "format"; + case ExceptionArgument.culture: + return "culture"; + case ExceptionArgument.comparer: + return "comparer"; + case ExceptionArgument.comparable: + return "comparable"; + case ExceptionArgument.source: + return "source"; + case ExceptionArgument.state: + return "state"; + case ExceptionArgument.length: + return "length"; + case ExceptionArgument.comparisonType: + return "comparisonType"; + case ExceptionArgument.manager: + return "manager"; + case ExceptionArgument.sourceBytesToCopy: + return "sourceBytesToCopy"; + case ExceptionArgument.callBack: + return "callBack"; + case ExceptionArgument.creationOptions: + return "creationOptions"; + case ExceptionArgument.function: + return "function"; + case ExceptionArgument.delay: + return "delay"; + case ExceptionArgument.millisecondsDelay: + return "millisecondsDelay"; + case ExceptionArgument.millisecondsTimeout: + return "millisecondsTimeout"; + case ExceptionArgument.timeout: + return "timeout"; + case ExceptionArgument.type: + return "type"; + case ExceptionArgument.sourceIndex: + return "sourceIndex"; + case ExceptionArgument.sourceArray: + return "sourceArray"; + case ExceptionArgument.destinationIndex: + return "destinationIndex"; + case ExceptionArgument.destinationArray: + return "destinationArray"; + case ExceptionArgument.other: + return "other"; + case ExceptionArgument.newSize: + return "newSize"; + case ExceptionArgument.lowerBounds: + return "lowerBounds"; + case ExceptionArgument.lengths: + return "lengths"; + case ExceptionArgument.len: + return "len"; + case ExceptionArgument.keys: + return "keys"; + case ExceptionArgument.indices: + return "indices"; + case ExceptionArgument.endIndex: + return "endIndex"; + case ExceptionArgument.elementType: + return "elementType"; + case ExceptionArgument.arrayIndex: + return "arrayIndex"; + default: + Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); + return argument.ToString(); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionResource enum value to the resource string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetResourceString(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return SR.GetResourceString(resource.ToString()); + } +#endif + + private static string GetResourceString(ExceptionResource resource) + { + switch (resource) + { + case ExceptionResource.ArgumentOutOfRange_Index: + return "Argument 'index' was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_Count: + return "Argument 'count' was out of the range of valid values."; + case ExceptionResource.Arg_ArrayPlusOffTooSmall: + return "Array plus offset too small."; + case ExceptionResource.NotSupported_ReadOnlyCollection: + return "This operation is not supported on a read-only collection."; + case ExceptionResource.Arg_RankMultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_NonZeroLowerBound: + return "Arrays with a non-zero lower bound are not supported."; + case ExceptionResource.ArgumentOutOfRange_ListInsert: + return "Insertion index was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum: + return "The number must be non-negative."; + case ExceptionResource.ArgumentOutOfRange_SmallCapacity: + return "The capacity cannot be set below the current Count."; + case ExceptionResource.Argument_InvalidOffLen: + return "Invalid offset length."; + case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection: + return "The given value was larger than the size of the collection."; + case ExceptionResource.Serialization_MissingKeys: + return "Serialization error: missing keys."; + case ExceptionResource.Serialization_NullKey: + return "Serialization error: null key."; + case ExceptionResource.NotSupported_KeyCollectionSet: + return "The KeyCollection does not support modification."; + case ExceptionResource.NotSupported_ValueCollectionSet: + return "The ValueCollection does not support modification."; + case ExceptionResource.InvalidOperation_NullArray: + return "Null arrays are not supported."; + case ExceptionResource.InvalidOperation_HSCapacityOverflow: + return "Set hash capacity overflow. Cannot increase size."; + case ExceptionResource.NotSupported_StringComparison: + return "String comparison not supported."; + case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported: + return "SyncRoot not supported."; + case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength: + return "The other array is not of the correct length."; + case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex: + return "The end index does not come after the start index."; + case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported: + return "Huge arrays are not supported."; + case ExceptionResource.Argument_AddingDuplicate: + return "Duplicate item added."; + case ExceptionResource.Argument_InvalidArgumentForComparison: + return "Invalid argument for comparison."; + case ExceptionResource.Arg_LowerBoundsMustMatch: + return "Array lower bounds must match."; + case ExceptionResource.Arg_MustBeType: + return "Argument must be of type: "; + case ExceptionResource.InvalidOperation_IComparerFailed: + return "IComparer failed."; + case ExceptionResource.NotSupported_FixedSizeCollection: + return "This operation is not suppored on a fixed-size collection."; + case ExceptionResource.Rank_MultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_TypeNotSupported: + return "Type not supported."; + default: + Debug.Assert(false, + "The enum value is not defined, please check the ExceptionResource Enum."); + return resource.ToString(); + } + } + } + + // + // The convention for this enum is using the argument name as the enum name + // + internal enum ExceptionArgument + { + obj, + dictionary, + array, + info, + key, + text, + values, + value, + startIndex, + task, + ch, + s, + input, + list, + index, + capacity, + collection, + item, + converter, + match, + count, + action, + comparison, + exceptions, + exception, + enumerable, + start, + format, + culture, + comparer, + comparable, + source, + state, + length, + comparisonType, + manager, + sourceBytesToCopy, + callBack, + creationOptions, + function, + delay, + millisecondsDelay, + millisecondsTimeout, + timeout, + type, + sourceIndex, + sourceArray, + destinationIndex, + destinationArray, + other, + newSize, + lowerBounds, + lengths, + len, + keys, + indices, + endIndex, + elementType, + arrayIndex + } + + // + // The convention for this enum is using the resource name as the enum name + // + internal enum ExceptionResource + { + ArgumentOutOfRange_Index, + ArgumentOutOfRange_Count, + Arg_ArrayPlusOffTooSmall, + NotSupported_ReadOnlyCollection, + Arg_RankMultiDimNotSupported, + Arg_NonZeroLowerBound, + ArgumentOutOfRange_ListInsert, + ArgumentOutOfRange_NeedNonNegNum, + ArgumentOutOfRange_SmallCapacity, + Argument_InvalidOffLen, + ArgumentOutOfRange_BiggerThanCollection, + Serialization_MissingKeys, + Serialization_NullKey, + NotSupported_KeyCollectionSet, + NotSupported_ValueCollectionSet, + InvalidOperation_NullArray, + InvalidOperation_HSCapacityOverflow, + NotSupported_StringComparison, + ConcurrentCollection_SyncRoot_NotSupported, + ArgumentException_OtherNotArrayOfCorrectLength, + ArgumentOutOfRange_EndIndexStartIndex, + ArgumentOutOfRange_HugeArrayNotSupported, + Argument_AddingDuplicate, + Argument_InvalidArgumentForComparison, + Arg_LowerBoundsMustMatch, + Arg_MustBeType, + InvalidOperation_IComparerFailed, + NotSupported_FixedSizeCollection, + Rank_MultiDimNotSupported, + Arg_TypeNotSupported, + } +} diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 7c55321a80..9a2cc1bfde 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -30,6 +30,12 @@ namespace Avalonia.Data /// Represents a binding notification that can be a valid binding value, or a binding or /// data validation error. /// + /// + /// This class is very similar to , but where + /// is used by typed bindings, this class is used to hold binding and data validation errors in + /// untyped bindings. As Avalonia moves towards using typed bindings by default we may want to remove + /// this class. + /// public class BindingNotification { /// @@ -236,6 +242,26 @@ namespace Avalonia.Data _value = value; } + public BindingValue ToBindingValue() + { + if (ErrorType == BindingErrorType.None) + { + return HasValue ? new BindingValue(Value) : BindingValue.Unset; + } + else if (ErrorType == BindingErrorType.Error) + { + return BindingValue.BindingError( + Error, + HasValue ? new Optional(Value) : Optional.Empty); + } + else + { + return BindingValue.DataValidationError( + Error, + HasValue ? new Optional(Value) : Optional.Empty); + } + } + /// public override string ToString() { diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 256de2f902..1b47cc7490 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -4,12 +4,13 @@ using System; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Data { public static class BindingOperations { - public static readonly object DoNothing = new object(); + public static readonly object DoNothing = new DoNothingType(); /// /// Applies an a property on an . @@ -63,7 +64,10 @@ namespace Avalonia.Data return source .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Take(1) - .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority)); + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); } else { @@ -88,4 +92,15 @@ namespace Avalonia.Data } } } + + public sealed class DoNothingType + { + internal DoNothingType() { } + + /// + /// Returns the string representation of . + /// + /// The string "(do nothing)". + public override string ToString() => "(do nothing)"; + } } diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs new file mode 100644 index 0000000000..cecdd33e7b --- /dev/null +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -0,0 +1,432 @@ +using System; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Data +{ + /// + /// Describes the type of a . + /// + [Flags] + public enum BindingValueType + { + /// + /// An unset value: the target property will revert to its unbound state until a new + /// binding value is produced. + /// + UnsetValue = 0, + + /// + /// Do nothing: the binding value will be ignored. + /// + DoNothing = 1, + + /// + /// A simple value. + /// + Value = 2 | HasValue, + + /// + /// A binding error, such as a missing source property. + /// + BindingError = 3 | HasError, + + /// + /// A data validation error. + /// + DataValidationError = 4 | HasError, + + /// + /// A binding error with a fallback value. + /// + BindingErrorWithFallback = BindingError | HasValue, + + /// + /// A data validation error with a fallback value. + /// + DataValidationErrorWithFallback = DataValidationError | HasValue, + + TypeMask = 0x00ff, + HasValue = 0x0100, + HasError = 0x0200, + } + + /// + /// A value passed into a binding. + /// + /// The value type. + /// + /// The avalonia binding system is typed, and as such additional state is stored in this + /// structure. A binding value can be in a number of states, described by the + /// property: + /// + /// - : a simple value + /// - : the target property will revert to its unbound + /// state until a new binding value is produced. Represented by + /// in an untyped context + /// - : the binding value will be ignored. Represented + /// by in an untyped context + /// - : a binding error, such as a missing source + /// property, with an optional fallback value + /// - : a data validation error, with an + /// optional fallback value + /// + /// To create a new binding value you can: + /// + /// - For a simple value, call the constructor or use an implicit + /// conversion from + /// - For an unset value, use or simply `default` + /// - For other types, call one of the static factory methods + /// + public readonly struct BindingValue + { + private readonly T _value; + + /// + /// Initializes a new instance of the struct with a type of + /// + /// + /// The value. + public BindingValue(T value) + { + ValidateValue(value); + _value = value; + Type = BindingValueType.Value; + Error = null; + } + + private BindingValue(BindingValueType type, T value, Exception? error) + { + _value = value; + Type = type; + Error = error; + } + + /// + /// Gets a value indicating whether the binding value represents either a binding or data + /// validation error. + /// + public bool HasError => Type.HasFlagCustom(BindingValueType.HasError); + + /// + /// Gets a value indicating whether the binding value has a value. + /// + public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue); + + /// + /// Gets the type of the binding value. + /// + public BindingValueType Type { get; } + + /// + /// Gets the binding value or fallback value. + /// + /// + /// is false. + /// + public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value."); + + /// + /// Gets the binding or data validation error. + /// + public Exception? Error { get; } + + /// + /// Converts the binding value to an . + /// + /// + public Optional ToOptional() => HasValue ? new Optional(_value) : default; + + /// + public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)"; + + /// + /// Converts the value to untyped representation, using , + /// and where + /// appropriate. + /// + /// The untyped representation of the binding value. + public object? ToUntyped() + { + return Type switch + { + BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, + BindingValueType.DoNothing => BindingOperations.DoNothing, + BindingValueType.Value => _value, + BindingValueType.BindingError => + new BindingNotification(Error, BindingErrorType.Error), + BindingValueType.BindingErrorWithFallback => + new BindingNotification(Error, BindingErrorType.Error, Value), + BindingValueType.DataValidationError => + new BindingNotification(Error, BindingErrorType.DataValidationError), + BindingValueType.DataValidationErrorWithFallback => + new BindingNotification(Error, BindingErrorType.DataValidationError, Value), + _ => throw new NotSupportedException("Invalid BindingValueType."), + }; + } + + /// + /// Returns a new binding value with the specified value. + /// + /// The new value. + /// The new binding value. + /// + /// The binding type is or + /// . + /// + public BindingValue WithValue(T value) + { + if (Type == BindingValueType.DoNothing) + { + throw new InvalidOperationException("Cannot add value to DoNothing binding value."); + } + + var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type; + return new BindingValue(type | BindingValueType.HasValue, value, Error); + } + + /// + /// Gets the value of the binding value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value of the binding value if present, otherwise a default value. + /// + /// The default value. + /// The value. + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } + + /// + /// Gets the value of the binding value if present, otherwise a default value. + /// + /// The default value. + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// present but not of the correct type or null, or if the + /// value is not present. + /// + public TResult GetValueOrDefault(TResult defaultValue) + { + return HasValue ? + _value is TResult result ? result : default + : defaultValue; + } + + /// + /// Creates a from an object, handling the special values + /// and . + /// + /// The untyped value. + /// The typed binding value. + public static BindingValue FromUntyped(object? value) + { + return value switch + { + UnsetValueType _ => Unset, + DoNothingType _ => DoNothing, + BindingNotification n => n.ToBindingValue().Cast(), + _ => (T)value + }; + } + + /// + /// Creates a binding value from an instance of the underlying value type. + /// + /// The value. + public static implicit operator BindingValue(T value) => new BindingValue(value); + + /// + /// Creates a binding value from an . + /// + /// The optional value. + + public static implicit operator BindingValue(Optional optional) + { + return optional.HasValue ? optional.Value : Unset; + } + + /// + /// Returns a binding value with a type of . + /// + public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null); + + /// + /// Returns a binding value with a type of . + /// + public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null); + + /// + /// Returns a binding value with a type of . + /// + /// The binding error. + public static BindingValue BindingError(Exception e) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.BindingError, default, e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The binding error. + /// The fallback value. + public static BindingValue BindingError(Exception e, T fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); + } + + /// + /// Returns a binding value with a type of or + /// . + /// + /// The binding error. + /// The fallback value. + public static BindingValue BindingError(Exception e, Optional fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue( + fallbackValue.HasValue ? + BindingValueType.BindingErrorWithFallback : + BindingValueType.BindingError, + fallbackValue.HasValue ? fallbackValue.Value : default, + e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The data validation error. + public static BindingValue DataValidationError(Exception e) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.DataValidationError, default, e); + } + + /// + /// Returns a binding value with a type of . + /// + /// The data validation error. + /// The fallback value. + public static BindingValue DataValidationError(Exception e, T fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); + } + + /// + /// Returns a binding value with a type of or + /// . + /// + /// The binding error. + /// The fallback value. + public static BindingValue DataValidationError(Exception e, Optional fallbackValue) + { + e = e ?? throw new ArgumentNullException("e"); + + return new BindingValue( + fallbackValue.HasValue ? + BindingValueType.DataValidationError : + BindingValueType.DataValidationErrorWithFallback, + fallbackValue.HasValue ? fallbackValue.Value : default, + e); + } + + private static void ValidateValue(T value) + { + if (value is UnsetValueType) + { + throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>."); + } + + if (value is DoNothingType) + { + throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>."); + } + + if (value is BindingValue) + { + throw new InvalidOperationException("BindingValue cannot be wrapped in a BindingValue<>."); + } + } + } + + public static class BindingValueExtensions + { + /// + /// Casts the type of a using only the C# cast operator. + /// + /// The target type. + /// The binding value. + /// The cast value. + public static BindingValue Cast(this BindingValue value) + { + return value.Type switch + { + BindingValueType.DoNothing => BindingValue.DoNothing, + BindingValueType.UnsetValue => BindingValue.Unset, + BindingValueType.Value => new BindingValue((T)value.Value), + BindingValueType.BindingError => BindingValue.BindingError(value.Error!), + BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( + value.Error!, + (T)value.Value), + BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), + BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( + value.Error!, + (T)value.Value), + _ => throw new NotSupportedException("Invalid BindingValue type."), + }; + } + + /// + /// Casts the type of a using the implicit conversions + /// allowed by the C# language. + /// + /// The target type. + /// The binding value. + /// The cast value. + /// + /// Note that this method uses reflection and as such may be slow. + /// + public static BindingValue Convert(this BindingValue value) + { + return value.Type switch + { + BindingValueType.DoNothing => BindingValue.DoNothing, + BindingValueType.UnsetValue => BindingValue.Unset, + BindingValueType.Value => new BindingValue(TypeUtilities.ConvertImplicit(value.Value)), + BindingValueType.BindingError => BindingValue.BindingError(value.Error!), + BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( + value.Error!, + TypeUtilities.ConvertImplicit(value.Value)), + BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), + BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( + value.Error!, + TypeUtilities.ConvertImplicit(value.Value)), + _ => throw new NotSupportedException("Invalid BindingValue type."), + }; + } + } +} diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs new file mode 100644 index 0000000000..dd952c895c --- /dev/null +++ b/src/Avalonia.Base/Data/Optional.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Avalonia.Data +{ + /// + /// An optional typed value. + /// + /// The value type. + /// + /// This struct is similar to except it also accepts reference types: + /// note that null is a valid value for reference types. It is also similar to + /// but has only two states: "value present" and "value missing". + /// + /// To create a new optional value you can: + /// + /// - For a simple value, call the constructor or use an implicit + /// conversion from + /// - For an missing value, use or simply `default` + /// + public readonly struct Optional : IEquatable> + { + private readonly T _value; + + /// + /// Initializes a new instance of the struct with value. + /// + /// The value. + public Optional(T value) + { + _value = value; + HasValue = true; + } + + /// + /// Gets a value indicating whether a value is present. + /// + public bool HasValue { get; } + + /// + /// Gets the value. + /// + /// + /// is false. + /// + public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); + + /// + public override bool Equals(object obj) => obj is Optional o && this == o; + + /// + public bool Equals(Optional other) => this == other; + + /// + public override int GetHashCode() => HasValue ? _value?.GetHashCode() ?? 0 : 0; + + /// + /// Casts the value (if any) to an . + /// + /// The cast optional value. + public Optional ToObject() => HasValue ? new Optional(_value) : default; + + /// + public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; + + /// + /// Gets the value if present, otherwise a default value. + /// + /// The default value. + /// The value. + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } + + /// + /// Gets the value if present, otherwise a default value. + /// + /// The default value. + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// present but not of the correct type or null, or if the + /// value is not present. + /// + public TResult GetValueOrDefault(TResult defaultValue) + { + return HasValue ? + _value is TResult result ? result : default + : defaultValue; + } + + /// + /// Creates an from an instance of the underlying value type. + /// + /// The value. + public static implicit operator Optional(T value) => new Optional(value); + + /// + /// Compares two s for inequality. + /// + /// The first value. + /// The second value. + /// True if the values are unequal; otherwise false. + public static bool operator !=(Optional x, Optional y) => !(x == y); + + /// + /// Compares two s for equality. + /// + /// The first value. + /// The second value. + /// True if the values are equal; otherwise false. + public static bool operator==(Optional x, Optional y) + { + if (!x.HasValue && !y.HasValue) + { + return true; + } + else if (x.HasValue && y.HasValue) + { + return EqualityComparer.Default.Equals(x.Value, y.Value); + } + else + { + return false; + } + } + + /// + /// Returns an without a value. + /// + public static Optional Empty => default; + } +} diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index 7afbcabd2a..d062856a73 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.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 Avalonia.Data; namespace Avalonia.Diagnostics @@ -21,35 +22,7 @@ namespace Avalonia.Diagnostics /// public static AvaloniaPropertyValue GetDiagnostic(this AvaloniaObject o, AvaloniaProperty property) { - var set = o.GetSetValues(); - - if (set.TryGetValue(property, out var obj)) - { - if (obj is PriorityValue value) - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - (BindingPriority)value.ValuePriority, - value.GetDiagnostic()); - } - else - { - return new AvaloniaPropertyValue( - property, - obj, - BindingPriority.LocalValue, - "Local value"); - } - } - else - { - return new AvaloniaPropertyValue( - property, - o.GetValue(property), - BindingPriority.Unset, - "Unset"); - } + return o.GetDiagnosticInternal(property); } } } diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 1ce73c20ba..2a8c731614 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -16,7 +16,7 @@ namespace Avalonia /// system. They hold a getter and an optional setter which /// allows the avalonia property system to read and write the current value. /// - public class DirectProperty : AvaloniaProperty, IDirectPropertyAccessor + public class DirectProperty : DirectPropertyBase, IDirectPropertyAccessor where TOwner : IAvaloniaObject { /// @@ -26,12 +26,16 @@ namespace Avalonia /// Gets the current value of the property. /// Sets the value of the property. May be null. /// The property metadata. + /// + /// Whether the property is interested in data validation. + /// public DirectProperty( string name, Func getter, Action setter, - DirectPropertyMetadata metadata) - : base(name, typeof(TOwner), metadata) + DirectPropertyMetadata metadata, + bool enableDataValidation) + : base(name, typeof(TOwner), metadata, enableDataValidation) { Contract.Requires(getter != null); @@ -46,12 +50,16 @@ namespace Avalonia /// Gets the current value of the property. /// Sets the value of the property. May be null. /// Optional overridden metadata. + /// + /// Whether the property is interested in data validation. + /// private DirectProperty( - AvaloniaProperty source, + DirectPropertyBase source, Func getter, Action setter, - DirectPropertyMetadata metadata) - : base(source, typeof(TOwner), metadata) + DirectPropertyMetadata metadata, + bool enableDataValidation) + : base(source, typeof(TOwner), metadata, enableDataValidation) { Contract.Requires(getter != null); @@ -65,6 +73,9 @@ namespace Avalonia /// public override bool IsReadOnly => Setter == null; + /// + public override Type Owner => typeof(TOwner); + /// /// Gets the getter function. /// @@ -75,9 +86,6 @@ namespace Avalonia /// public Action Setter { get; } - /// - Type IDirectPropertyAccessor.Owner => typeof(TOwner); - /// /// Registers the direct property on another type. /// @@ -99,6 +107,45 @@ namespace Avalonia BindingMode defaultBindingMode = BindingMode.Default, bool enableDataValidation = false) where TNewOwner : AvaloniaObject + { + var metadata = new DirectPropertyMetadata( + unsetValue: unsetValue, + defaultBindingMode: defaultBindingMode); + + metadata.Merge(GetMetadata(), this); + + var result = new DirectProperty( + (DirectPropertyBase)this, + getter, + setter, + metadata, + enableDataValidation); + + AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); + return result; + } + + /// + /// Registers the direct property on another type. + /// + /// The type of the additional owner. + /// Gets the current value of the property. + /// Sets the value of the property. + /// + /// The value to use when the property is set to + /// + /// The default binding mode for the property. + /// + /// Whether the property is interested in data validation. + /// + /// The property. + public DirectProperty AddOwnerWithDataValidation( + Func getter, + Action setter, + TValue unsetValue = default(TValue), + BindingMode defaultBindingMode = BindingMode.Default, + bool enableDataValidation = false) + where TNewOwner : AvaloniaObject { var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, @@ -111,12 +158,33 @@ namespace Avalonia this, getter, setter, - metadata); + metadata, + enableDataValidation); AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); return result; } + /// + internal override TValue InvokeGetter(IAvaloniaObject instance) + { + return Getter((TOwner)instance); + } + + /// + internal override void InvokeSetter(IAvaloniaObject instance, BindingValue value) + { + if (Setter == null) + { + throw new ArgumentException($"The property {Name} is readonly."); + } + + if (value.HasValue) + { + Setter((TOwner)instance, value.Value); + } + } + /// object IDirectPropertyAccessor.GetValue(IAvaloniaObject instance) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs new file mode 100644 index 0000000000..7a0be065eb --- /dev/null +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -0,0 +1,168 @@ +using System; +using Avalonia.Data; +using Avalonia.Reactive; + +#nullable enable + +namespace Avalonia +{ + /// + /// Base class for direct properties. + /// + /// The type of the property's value. + /// + /// Whereas is typed on the owner type, this base + /// class provides a non-owner-typed interface to a direct poperty. + /// + public abstract class DirectPropertyBase : AvaloniaProperty + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the class that registers the property. + /// The property metadata. + /// + /// Whether the property is interested in data validation. + /// + protected DirectPropertyBase( + string name, + Type ownerType, + PropertyMetadata metadata, + bool enableDataValidation) + : base(name, ownerType, metadata) + { + IsDataValidationEnabled = enableDataValidation; + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to copy. + /// The new owner type. + /// Optional overridden metadata. + /// + /// Whether the property is interested in data validation. + /// + protected DirectPropertyBase( + AvaloniaProperty source, + Type ownerType, + PropertyMetadata metadata, + bool enableDataValidation) + : base(source, ownerType, metadata) + { + IsDataValidationEnabled = enableDataValidation; + } + + /// + /// Gets the type that registered the property. + /// + public abstract Type Owner { get; } + + /// + /// Gets a value that indicates whether data validation is enabled for the property. + /// + public bool IsDataValidationEnabled { get; } + + /// + /// Gets the value of the property on the instance. + /// + /// The instance. + /// The property value. + internal abstract TValue InvokeGetter(IAvaloniaObject instance); + + /// + /// Sets the value of the property on the instance. + /// + /// The instance. + /// The value. + internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue value); + + /// + /// Gets the unset value for the property on the specified type. + /// + /// The type. + /// The unset value. + public TValue GetUnsetValue(Type type) + { + type = type ?? throw new ArgumentNullException(nameof(type)); + return GetMetadata(type).UnsetValue; + } + + /// + /// Gets the property metadata for the specified type. + /// + /// The type. + /// + /// The property metadata. + /// + public new DirectPropertyMetadata GetMetadata(Type type) + { + return (DirectPropertyMetadata)base.GetMetadata(type); + } + + /// + internal override void NotifyInitialized(IAvaloniaObject o) + { + if (HasNotifyInitializedObservers) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + InvokeGetter(o), + BindingPriority.Unset); + NotifyInitialized(e); + } + } + + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); + } + + /// + internal override object? RouteGetValue(IAvaloniaObject o) + { + return o.GetValue(this); + } + + /// + internal override void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) + { + var v = TryConvert(value); + + if (v.HasValue) + { + o.SetValue(this, (TValue)v.Value); + } + else if (v.Type == BindingValueType.UnsetValue) + { + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error!; + } + } + + /// + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter); + } + + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) + { + throw new NotSupportedException("Direct properties do not support inheritance."); + } + } +} diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 5a3829167a..fb85ae222c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -17,16 +17,24 @@ namespace Avalonia event EventHandler PropertyChanged; /// - /// Raised when an inheritable value changes on this object. + /// Clears an 's local value. /// - event EventHandler InheritablePropertyChanged; + /// The property. + void ClearValue(StyledPropertyBase property); + + /// + /// Clears an 's local value. + /// + /// The property. + void ClearValue(DirectPropertyBase property); /// /// Gets a value. /// + /// The type of the property. /// The property. /// The value. - object GetValue(AvaloniaProperty property); + T GetValue(StyledPropertyBase property); /// /// Gets a value. @@ -34,7 +42,7 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - T GetValue(AvaloniaProperty property); + T GetValue(DirectPropertyBase property); /// /// Checks whether a is animating. @@ -53,12 +61,13 @@ namespace Avalonia /// /// Sets a value. /// + /// The type of the property. /// The property. /// The value. /// The priority of the value. - void SetValue( - AvaloniaProperty property, - object value, + void SetValue( + StyledPropertyBase property, + T value, BindingPriority priority = BindingPriority.LocalValue); /// @@ -67,24 +76,21 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - /// The priority of the value. - void SetValue( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue); + void SetValue(DirectPropertyBase property, T value); /// /// Binds a to an observable. /// + /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// - IDisposable Bind( - AvaloniaProperty property, - IObservable source, + IDisposable Bind( + StyledPropertyBase property, + IObservable> source, BindingPriority priority = BindingPriority.LocalValue); /// @@ -93,13 +99,52 @@ namespace Avalonia /// The type of the property. /// The property. /// The observable. - /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// IDisposable Bind( + DirectPropertyBase property, + IObservable> source); + + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + void CoerceValue(StyledPropertyBase property); + + /// + /// Registers an object as an inheritance child. + /// + /// The inheritance child. + /// + /// Inheritance children will receive a call to + /// + /// when an inheritable property value changes on the parent. + /// + void AddInheritanceChild(IAvaloniaObject child); + + /// + /// Unregisters an object as an inheritance child. + /// + /// The inheritance child. + /// + /// Removes an inheritance child that was added by a call to + /// . + /// + void RemoveInheritanceChild(IAvaloniaObject child); + + /// + /// Called when an inheritable property changes on an object registered as an inheritance + /// parent. + /// + /// The type of the value. + /// The property that has changed. + /// + /// + void InheritedPropertyChanged( AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue); + Optional oldValue, + Optional newValue); } } diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs deleted file mode 100644 index 1d6e5e59ad..0000000000 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ /dev/null @@ -1,51 +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 Avalonia.Data; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// An owner of a . - /// - internal interface IPriorityValueOwner - { - /// - /// Called when a 's value changes. - /// - /// The the property that has changed. - /// The priority of the value. - /// The old value. - /// The new value. - void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue); - - /// - /// Called when a is received by a - /// . - /// - /// The the property that has changed. - /// The notification. - void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); - - /// - /// Returns deferred setter for given non-direct property. - /// - /// Property. - /// Deferred setter for given property. - DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); - - /// - /// Logs a binding error. - /// - /// The property the error occurred on. - /// The binding error. - void LogError(AvaloniaProperty property, Exception e); - - /// - /// Ensures that the current thread is the UI thread. - /// - void VerifyAccess(); - } -} diff --git a/src/Avalonia.Base/IStyledPropertyAccessor.cs b/src/Avalonia.Base/IStyledPropertyAccessor.cs index f2ec5bd33f..dfa0208c38 100644 --- a/src/Avalonia.Base/IStyledPropertyAccessor.cs +++ b/src/Avalonia.Base/IStyledPropertyAccessor.cs @@ -18,14 +18,5 @@ namespace Avalonia /// The default value. /// object GetDefaultValue(Type type); - - /// - /// Gets a validation function for the property on the specified type. - /// - /// The type. - /// - /// The validation function, or null if no validation function exists. - /// - Func GetValidationFunc(Type type); } } diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index 22cda075fa..cc92e21261 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -14,10 +14,5 @@ namespace Avalonia /// Gets the default value for the property. /// object DefaultValue { get; } - - /// - /// Gets the property's validation function. - /// - Func Validate { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs deleted file mode 100644 index 7f5415c2d8..0000000000 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ /dev/null @@ -1,160 +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.Runtime.ExceptionServices; -using Avalonia.Data; -using Avalonia.Threading; - -namespace Avalonia -{ - /// - /// A registered binding in a . - /// - internal class PriorityBindingEntry : IDisposable, IObserver - { - private readonly PriorityLevel _owner; - private IDisposable _subscription; - - /// - /// Initializes a new instance of the class. - /// - /// The owner. - /// - /// The binding index. Later bindings should have higher indexes. - /// - public PriorityBindingEntry(PriorityLevel owner, int index) - { - _owner = owner; - Index = index; - } - - /// - /// Gets the observable associated with the entry. - /// - public IObservable Observable { get; private set; } - - /// - /// Gets a description of the binding. - /// - public string Description - { - get; - private set; - } - - /// - /// Gets the binding entry index. Later bindings will have higher indexes. - /// - public int Index - { - get; - } - - /// - /// Gets a value indicating whether the binding has completed. - /// - public bool HasCompleted { get; private set; } - - /// - /// The current value of the binding. - /// - public object Value - { - get; - private set; - } - - /// - /// Starts listening to the binding. - /// - /// The binding. - public void Start(IObservable binding) - { - Contract.Requires(binding != null); - - if (_subscription != null) - { - throw new Exception("PriorityValue.Entry.Start() called more than once."); - } - - Observable = binding; - Value = AvaloniaProperty.UnsetValue; - - if (binding is IDescription) - { - Description = ((IDescription)binding).Description; - } - - _subscription = binding.Subscribe(this); - } - - /// - /// Ends the binding subscription. - /// - public void Dispose() - { - _subscription?.Dispose(); - } - - void IObserver.OnNext(object value) - { - void Signal(PriorityBindingEntry instance, object newValue) - { - var notification = newValue as BindingNotification; - - if (notification != null) - { - if (notification.HasValue || notification.ErrorType == BindingErrorType.Error) - { - instance.Value = notification.Value; - instance._owner.Changed(instance); - } - - if (notification.ErrorType != BindingErrorType.None) - { - instance._owner.Error(instance, notification); - } - } - else - { - instance.Value = newValue; - instance._owner.Changed(instance); - } - } - - if (Dispatcher.UIThread.CheckAccess()) - { - Signal(this, value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - - Dispatcher.UIThread.Post(() => Signal(instance, newValue)); - } - } - - void IObserver.OnCompleted() - { - HasCompleted = true; - - if (Dispatcher.UIThread.CheckAccess()) - { - _owner.Completed(this); - } - else - { - Dispatcher.UIThread.Post(() => _owner.Completed(this)); - } - } - - void IObserver.OnError(Exception error) - { - ExceptionDispatchInfo.Capture(error).Throw(); - } - } -} diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs deleted file mode 100644 index a2364083ea..0000000000 --- a/src/Avalonia.Base/PriorityLevel.cs +++ /dev/null @@ -1,227 +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.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using Avalonia.Data; - -namespace Avalonia -{ - /// - /// Stores bindings for a priority level in a . - /// - /// - /// - /// Each priority level in a has a current , - /// a list of and a . When there are no - /// bindings present, or all bindings return then - /// Value will equal DirectValue. - /// - /// - /// When there are bindings present, then the latest added binding that doesn't return - /// UnsetValue will take precedence. The active binding is returned by the - /// property (which refers to the active binding's - /// property rather than the index in - /// Bindings). - /// - /// - /// If DirectValue is set while a binding is active, then it will replace the - /// current value until the active binding fires again. - /// - /// - internal class PriorityLevel - { - private object _directValue; - private int _nextIndex; - - /// - /// Initializes a new instance of the class. - /// - /// The owner. - /// The priority. - public PriorityLevel( - PriorityValue owner, - int priority) - { - Contract.Requires(owner != null); - - Owner = owner; - Priority = priority; - Value = _directValue = AvaloniaProperty.UnsetValue; - ActiveBindingIndex = -1; - Bindings = new LinkedList(); - } - - /// - /// Gets the owner of the level. - /// - public PriorityValue Owner { get; } - - /// - /// Gets the priority of this level. - /// - public int Priority { get; } - - /// - /// Gets or sets the direct value for this priority level. - /// - public object DirectValue - { - get - { - return _directValue; - } - - set - { - Value = _directValue = value; - Owner.LevelValueChanged(this); - } - } - - /// - /// Gets the current binding for the priority level. - /// - public object Value { get; private set; } - - /// - /// Gets the value of the active binding, or -1 - /// if no binding is active. - /// - public int ActiveBindingIndex { get; private set; } - - /// - /// Gets the bindings for the priority level. - /// - public LinkedList Bindings { get; } - - /// - /// Adds a binding. - /// - /// The binding to add. - /// A disposable used to remove the binding. - public IDisposable Add(IObservable binding) - { - Contract.Requires(binding != null); - - var entry = new PriorityBindingEntry(this, _nextIndex++); - var node = Bindings.AddFirst(entry); - - entry.Start(binding); - - return new RemoveBindingDisposable(node, Bindings, this); - } - - /// - /// Invoked when an entry in changes value. - /// - /// The entry that changed. - public void Changed(PriorityBindingEntry entry) - { - if (entry.Index >= ActiveBindingIndex) - { - if (entry.Value != AvaloniaProperty.UnsetValue) - { - Value = entry.Value; - ActiveBindingIndex = entry.Index; - Owner.LevelValueChanged(this); - } - else - { - ActivateFirstBinding(); - } - } - } - - /// - /// Invoked when an entry in completes. - /// - /// The entry that completed. - public void Completed(PriorityBindingEntry entry) - { - Bindings.Remove(entry); - - if (entry.Index >= ActiveBindingIndex) - { - ActivateFirstBinding(); - } - } - - /// - /// Invoked when an entry in encounters a recoverable error. - /// - /// The entry that completed. - /// The error. - public void Error(PriorityBindingEntry entry, BindingNotification error) - { - Owner.LevelError(this, error); - } - - /// - /// Activates the first binding that has a value. - /// - private void ActivateFirstBinding() - { - foreach (var binding in Bindings) - { - if (binding.Value != AvaloniaProperty.UnsetValue) - { - Value = binding.Value; - ActiveBindingIndex = binding.Index; - Owner.LevelValueChanged(this); - return; - } - } - - Value = DirectValue; - ActiveBindingIndex = -1; - Owner.LevelValueChanged(this); - } - - private sealed class RemoveBindingDisposable : IDisposable - { - private readonly LinkedList _bindings; - private readonly PriorityLevel _priorityLevel; - private LinkedListNode _binding; - - public RemoveBindingDisposable( - LinkedListNode binding, - LinkedList bindings, - PriorityLevel priorityLevel) - { - _binding = binding; - _bindings = bindings; - _priorityLevel = priorityLevel; - } - - public void Dispose() - { - LinkedListNode binding = Interlocked.Exchange(ref _binding, null); - - if (binding == null) - { - // Some system is trying to remove binding twice. - Debug.Assert(false); - - return; - } - - PriorityBindingEntry entry = binding.Value; - - if (!entry.HasCompleted) - { - _bindings.Remove(binding); - - entry.Dispose(); - - if (entry.Index >= _priorityLevel.ActiveBindingIndex) - { - _priorityLevel.ActivateFirstBinding(); - } - } - } - } - } -} diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs deleted file mode 100644 index 61184ef7b1..0000000000 --- a/src/Avalonia.Base/PriorityValue.cs +++ /dev/null @@ -1,315 +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.Collections.Generic; -using System.Linq; -using System.Text; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Maintains a list of prioritized bindings together with a current value. - /// - /// - /// Bindings, in the form of s are added to the object using - /// the method. With the observable is passed a priority, where lower values - /// represent higher priorities. The current is selected from the highest - /// priority binding that doesn't return . Where there - /// are multiple bindings registered with the same priority, the most recently added binding - /// has a higher priority. Each time the value changes, the - /// method on the - /// owner object is fired with the old and new values. - /// - internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)> - { - private readonly Type _valueType; - private readonly SingleOrDictionary _levels = new SingleOrDictionary(); - private readonly Func _validate; - private (object value, int priority) _value; - private DeferredSetter _setter; - - /// - /// Initializes a new instance of the class. - /// - /// The owner of the object. - /// The property that the value represents. - /// The value type. - /// An optional validation function. - public PriorityValue( - IPriorityValueOwner owner, - AvaloniaProperty property, - Type valueType, - Func validate = null) - { - Owner = owner; - Property = property; - _valueType = valueType; - _value = (AvaloniaProperty.UnsetValue, int.MaxValue); - _validate = validate; - } - - /// - /// Gets a value indicating whether the property is animating. - /// - public bool IsAnimating - { - get - { - return ValuePriority <= (int)BindingPriority.Animation && - GetLevel(ValuePriority).ActiveBindingIndex != -1; - } - } - - /// - /// Gets the owner of the value. - /// - public IPriorityValueOwner Owner { get; } - - /// - /// Gets the property that the value represents. - /// - public AvaloniaProperty Property { get; } - - /// - /// Gets the current value. - /// - public object Value => _value.value; - - /// - /// Gets the priority of the binding that is currently active. - /// - public int ValuePriority => _value.priority; - - /// - /// Adds a new binding. - /// - /// The binding. - /// The binding priority. - /// - /// A disposable that will remove the binding. - /// - public IDisposable Add(IObservable binding, int priority) - { - return GetLevel(priority).Add(binding); - } - - /// - /// Sets the value for a specified priority. - /// - /// The value. - /// The priority - public void SetValue(object value, int priority) - { - GetLevel(priority).DirectValue = value; - } - - /// - /// Gets the currently active bindings on this object. - /// - /// An enumerable collection of bindings. - public IEnumerable GetBindings() - { - foreach (var level in _levels) - { - foreach (var binding in level.Value.Bindings) - { - yield return binding; - } - } - } - - /// - /// Returns diagnostic string that can help the user debug the bindings in effect on - /// this object. - /// - /// A diagnostic string. - public string GetDiagnostic() - { - var b = new StringBuilder(); - var first = true; - - foreach (var level in _levels) - { - if (!first) - { - b.AppendLine(); - } - - b.Append(ValuePriority == level.Key ? "*" : string.Empty); - b.Append("Priority "); - b.Append(level.Key); - b.Append(": "); - b.AppendLine(level.Value.Value?.ToString() ?? "(null)"); - b.AppendLine("--------"); - b.Append("Direct: "); - b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)"); - - foreach (var binding in level.Value.Bindings) - { - b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty); - b.Append(binding.Description ?? binding.Observable.GetType().Name); - b.Append(": "); - b.AppendLine(binding.Value?.ToString() ?? "(null)"); - } - - first = false; - } - - return b.ToString(); - } - - /// - /// Called when the value for a priority level changes. - /// - /// The priority level of the changed entry. - public void LevelValueChanged(PriorityLevel level) - { - if (level.Priority <= ValuePriority) - { - if (level.Value != AvaloniaProperty.UnsetValue) - { - UpdateValue(level.Value, level.Priority); - } - else - { - foreach (var i in _levels.Values.OrderBy(x => x.Priority)) - { - if (i.Value != AvaloniaProperty.UnsetValue) - { - UpdateValue(i.Value, i.Priority); - return; - } - } - - UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue); - } - } - } - - /// - /// Called when a priority level encounters an error. - /// - /// The priority level of the changed entry. - /// The binding error. - public void LevelError(PriorityLevel level, BindingNotification error) - { - Owner.LogError(Property, error.Error); - } - - /// - /// Causes a revalidation of the value. - /// - public void Revalidate() - { - if (_validate != null) - { - PriorityLevel level; - - if (_levels.TryGetValue(ValuePriority, out level)) - { - UpdateValue(level.Value, level.Priority); - } - } - } - - /// - /// Gets the with the specified priority, creating it if it - /// doesn't already exist. - /// - /// The priority. - /// The priority level. - private PriorityLevel GetLevel(int priority) - { - PriorityLevel result; - - if (!_levels.TryGetValue(priority, out result)) - { - result = new PriorityLevel(this, priority); - _levels.Add(priority, result); - } - - return result; - } - - /// - /// Updates the current and notifies all subscribers. - /// - /// The value to set. - /// The priority level that the value came from. - private void UpdateValue(object value, int priority) - { - var newValue = (value, priority); - - if (newValue == _value) - { - return; - } - - if (_setter == null) - { - _setter = Owner.GetNonDirectDeferredSetter(Property); - } - - _setter.SetAndNotifyCallback(Property, this, ref _value, newValue); - } - - void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value) - { - SetAndNotify(ref backing, value); - } - - private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update) - { - var val = update.value; - var notification = val as BindingNotification; - object castValue; - - if (notification != null) - { - val = (notification.HasValue) ? notification.Value : null; - } - - if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) - { - var old = backing.value; - - if (_validate != null && castValue != AvaloniaProperty.UnsetValue) - { - castValue = _validate(castValue); - } - - backing = (castValue, update.priority); - - if (notification?.HasValue == true) - { - notification.SetValue(castValue); - } - - if (notification == null || notification.HasValue) - { - Owner?.Changed(Property, ValuePriority, old, Value); - } - - if (notification != null) - { - Owner?.BindingNotificationReceived(Property, notification); - } - } - else - { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Binding, - Owner, - "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - Property.Name, - _valueType, - val, - val?.GetType()); - } - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs new file mode 100644 index 0000000000..09a0f169df --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -0,0 +1,107 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IBindingEntry : IPriorityValueEntry, IDisposable + { + } + + /// + /// Stores a binding in a or . + /// + /// The property type. + internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> + { + private readonly IAvaloniaObject _owner; + private IValueSink _sink; + private IDisposable? _subscription; + + public BindingEntry( + IAvaloniaObject owner, + StyledPropertyBase property, + IObservable> source, + BindingPriority priority, + IValueSink sink) + { + _owner = owner; + Property = property; + Source = source; + Priority = priority; + _sink = sink; + } + + public StyledPropertyBase Property { get; } + public BindingPriority Priority { get; } + public IObservable> Source { get; } + public Optional Value { get; private set; } + Optional IValue.Value => Value.ToObject(); + BindingPriority IValue.ValuePriority => Priority; + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + _sink.Completed(Property, this); + } + + public void OnCompleted() => _sink.Completed(Property, this); + + public void OnError(Exception error) + { + throw new NotImplementedException(); + } + + public void OnNext(BindingValue value) + { + if (Dispatcher.UIThread.CheckAccess()) + { + UpdateValue(value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + + Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); + } + } + + public void Start() + { + _subscription = Source.Subscribe(this); + } + + public void Reparent(IValueSink sink) => _sink = sink; + + private void UpdateValue(BindingValue value) + { + if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false) + { + value = Property.GetDefaultValue(_owner.GetType()); + } + + if (value.Type == BindingValueType.DoNothing) + { + return; + } + + var old = Value; + + if (value.Type != BindingValueType.DataValidationError) + { + Value = value.ToOptional(); + } + + _sink.ValueChanged(Property, Priority, old, value); + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs new file mode 100644 index 0000000000..f15f56e32b --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a value with a priority in a or + /// . + /// + /// The property type. + internal class ConstantValueEntry : IPriorityValueEntry + { + public ConstantValueEntry( + StyledPropertyBase property, + T value, + BindingPriority priority) + { + Property = property; + Value = value; + Priority = priority; + } + + public StyledPropertyBase Property { get; } + public BindingPriority Priority { get; } + public Optional Value { get; } + Optional IValue.Value => Value.ToObject(); + BindingPriority IValue.ValuePriority => Priority; + + public void Reparent(IValueSink sink) { } + } +} diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs new file mode 100644 index 0000000000..6ed6c2ef52 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IPriorityValueEntry : IValue + { + BindingPriority Priority { get; } + + void Reparent(IValueSink sink); + } + + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. + internal interface IPriorityValueEntry : IPriorityValueEntry, IValue + { + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs new file mode 100644 index 0000000000..0ce7fb8308 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -0,0 +1,24 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an untyped interface to . + /// + internal interface IValue + { + Optional Value { get; } + BindingPriority ValuePriority { get; } + } + + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. + internal interface IValue : IValue + { + new Optional Value { get; } + } +} diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs new file mode 100644 index 0000000000..223b0058c1 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -0,0 +1,20 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Represents an entity that can receive change notifications in a . + /// + internal interface IValueSink + { + void ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue); + + void Completed(AvaloniaProperty property, IPriorityValueEntry entry); + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs new file mode 100644 index 0000000000..22258390da --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -0,0 +1,22 @@ +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a value with local value priority in a or + /// . + /// + /// The property type. + internal class LocalValueEntry : IValue + { + private T _value; + + public LocalValueEntry(T value) => _value = value; + public Optional Value => _value; + public BindingPriority ValuePriority => BindingPriority.LocalValue; + Optional IValue.Value => Value.ToObject(); + public void SetValue(T value) => _value = value; + } +} diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs new file mode 100644 index 0000000000..2785dc6840 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.PropertyStore +{ + /// + /// Stores a set of prioritized values and bindings in a . + /// + /// The property type. + /// + /// When more than a single value or binding is applied to a property in an + /// , the entry in the is converted into + /// a . This class holds any number of + /// entries (sorted first by priority and then in the order + /// they were added) plus a local value. + /// + internal class PriorityValue : IValue, IValueSink + { + private readonly IAvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly List> _entries = new List>(); + private readonly Func? _coerceValue; + private Optional _localValue; + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink) + { + _owner = owner; + Property = property; + _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + IPriorityValueEntry existing) + : this(owner, property, sink) + { + existing.Reparent(this); + _entries.Add(existing); + + if (existing.Value.HasValue) + { + Value = existing.Value; + ValuePriority = existing.Priority; + } + } + + public PriorityValue( + IAvaloniaObject owner, + StyledPropertyBase property, + IValueSink sink, + LocalValueEntry existing) + : this(owner, property, sink) + { + _localValue = existing.Value; + Value = _localValue; + ValuePriority = BindingPriority.LocalValue; + } + + public StyledPropertyBase Property { get; } + public Optional Value { get; private set; } + public BindingPriority ValuePriority { get; private set; } + public IReadOnlyList> Entries => _entries; + Optional IValue.Value => Value.ToObject(); + + public void ClearLocalValue() => UpdateEffectiveValue(); + + public void SetValue(T value, BindingPriority priority) + { + if (priority == BindingPriority.LocalValue) + { + _localValue = value; + } + else + { + var insert = FindInsertPoint(priority); + _entries.Insert(insert, new ConstantValueEntry(Property, value, priority)); + } + + UpdateEffectiveValue(); + } + + public BindingEntry AddBinding(IObservable> source, BindingPriority priority) + { + var binding = new BindingEntry(_owner, Property, source, priority, this); + var insert = FindInsertPoint(binding.Priority); + _entries.Insert(insert, binding); + return binding; + } + + public void CoerceValue() => UpdateEffectiveValue(); + + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) + { + if (priority == BindingPriority.LocalValue) + { + _localValue = default; + } + + UpdateEffectiveValue(); + } + + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + { + _entries.Remove((IPriorityValueEntry)entry); + UpdateEffectiveValue(); + } + + private int FindInsertPoint(BindingPriority priority) + { + var result = _entries.Count; + + for (var i = 0; i < _entries.Count; ++i) + { + if (_entries[i].Priority < priority) + { + result = i; + break; + } + } + + return result; + } + + private void UpdateEffectiveValue() + { + var reachedLocalValues = false; + var value = default(Optional); + + if (_entries.Count > 0) + { + for (var i = _entries.Count - 1; i >= 0; --i) + { + var entry = _entries[i]; + + if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) + { + reachedLocalValues = true; + + if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + break; + } + } + + if (entry.Value.HasValue) + { + value = entry.Value; + ValuePriority = entry.Priority; + break; + } + } + } + else if (_localValue.HasValue) + { + value = _localValue; + ValuePriority = BindingPriority.LocalValue; + } + + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + + if (value != Value) + { + var old = Value; + Value = value; + _sink.ValueChanged(Property, ValuePriority, old, value); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs new file mode 100644 index 0000000000..be044b0559 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + +#nullable disable + public AvaloniaPropertyBindingObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } +#nullable enable + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + protected override void Subscribed(IObserver> observer, bool first) + { + observer.OnNext(new BindingValue(_value)); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValue = e.Sender.GetValue(typedArgs.Property); + + if (!typedArgs.OldValue.HasValue || !EqualityComparer.Default.Equals(newValue, _value)) + { + _value = newValue; + PublishNext(_value); + } + } + else + { + var newValue = e.Sender.GetValue(e.Property); + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } + } + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 4385ab13ef..238aba5c96 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,8 +44,22 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - _value = (T)e.NewValue; - PublishNext(_value); + T newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = typed.Sender.GetValue(typed.Property); + } + else + { + newValue = (T)e.Sender.GetValue(e.Property); + } + + if (!Equals(newValue, _value)) + { + _value = (T)newValue; + PublishNext(_value); + } } } } diff --git a/src/Avalonia.Base/Reactive/BindingValueAdapter.cs b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs new file mode 100644 index 0000000000..8c80e9f48c --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueAdapter.cs @@ -0,0 +1,61 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class BindingValueAdapter : SingleSubscriberObservableBase>, + IObserver + { + private readonly IObservable _source; + private IDisposable? _subscription; + + public BindingValueAdapter(IObservable source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(T value) => PublishNext(BindingValue.FromUntyped(value)); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class BindingValueSubjectAdapter : SingleSubscriberObservableBase>, + ISubject> + { + private readonly ISubject _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public BindingValueSubjectAdapter(ISubject source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + + public void OnNext(BindingValue value) + { + if (value.HasValue) + { + _source.OnNext(value.Value); + } + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver + { + private readonly BindingValueSubjectAdapter _owner; + + public Inner(BindingValueSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(T value) => _owner.PublishNext(BindingValue.FromUntyped(value)); + } + } +} diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs new file mode 100644 index 0000000000..6f0d29dd0f --- /dev/null +++ b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + public static class BindingValueExtensions + { + public static IObservable> ToBindingValue(this IObservable source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueAdapter(source); + } + + public static ISubject> ToBindingValue(this ISubject source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new BindingValueSubjectAdapter(source); + } + + public static IObservable ToUntyped(this IObservable> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingAdapter(source); + } + + public static ISubject ToUntyped(this ISubject> source) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + return new UntypedBindingSubjectAdapter(source); + } + } +} diff --git a/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs new file mode 100644 index 0000000000..bd9b31b100 --- /dev/null +++ b/src/Avalonia.Base/Reactive/TypedBindingAdapter.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Data; +using Avalonia.Logging; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class TypedBindingAdapter : SingleSubscriberObservableBase>, + IObserver> + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + private readonly IObservable> _source; + private IDisposable? _subscription; + + public TypedBindingAdapter( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + _target = target; + _property = property; + _source = source; + } + + public void OnNext(BindingValue value) + { + try + { + PublishNext(value.Convert()); + } + catch (InvalidCastException e) + { + Logger.TryGet(LogEventLevel.Error)?.Log( + LogArea.Binding, + _target, + "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", + _property.Name, + _property.PropertyType, + value.Value, + value.Value?.GetType()); + PublishNext(BindingValue.BindingError(e)); + } + } + + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + + public static IObservable> Create( + IAvaloniaObject target, + AvaloniaProperty property, + IObservable> source) + { + return source is IObservable> result ? + result : + new TypedBindingAdapter(target, property, source); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs new file mode 100644 index 0000000000..03c1afcea9 --- /dev/null +++ b/src/Avalonia.Base/Reactive/UntypedBindingAdapter.cs @@ -0,0 +1,57 @@ +using System; +using System.Reactive.Subjects; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Reactive +{ + internal class UntypedBindingAdapter : SingleSubscriberObservableBase, + IObserver> + { + private readonly IObservable> _source; + private IDisposable? _subscription; + + public UntypedBindingAdapter(IObservable> source) => _source = source; + public void OnCompleted() => PublishCompleted(); + public void OnError(Exception error) => PublishError(error); + public void OnNext(BindingValue value) => value.ToUntyped(); + protected override void Subscribed() => _subscription = _source.Subscribe(this); + protected override void Unsubscribed() => _subscription?.Dispose(); + } + + internal class UntypedBindingSubjectAdapter : SingleSubscriberObservableBase, + ISubject + { + private readonly ISubject> _source; + private readonly Inner _inner; + private IDisposable? _subscription; + + public UntypedBindingSubjectAdapter(ISubject> source) + { + _source = source; + _inner = new Inner(this); + } + + public void OnCompleted() => _source.OnCompleted(); + public void OnError(Exception error) => _source.OnError(error); + public void OnNext(object? value) + { + _source.OnNext(BindingValue.FromUntyped(value)); + } + + protected override void Subscribed() => _subscription = _source.Subscribe(_inner); + protected override void Unsubscribed() => _subscription?.Dispose(); + + private class Inner : IObserver> + { + private readonly UntypedBindingSubjectAdapter _owner; + + public Inner(UntypedBindingSubjectAdapter owner) => _owner = owner; + + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + public void OnNext(BindingValue value) => _owner.PublishNext(value.ToUntyped()); + } + } +} diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 4eb85a046e..62443b424c 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -17,14 +17,16 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. public StyledProperty( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) - : base(name, ownerType, metadata, inherits, notifying) + : base(name, ownerType, metadata, inherits, validate, notifying) { } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 27a502246a..8c4d683ae0 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -3,13 +3,15 @@ using System; using System.Diagnostics; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { /// /// Base class for styled properties. /// - public class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor + public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor { private bool _inherits; @@ -20,12 +22,14 @@ namespace Avalonia /// The type of the class that registers the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. /// A callback. protected StyledPropertyBase( string name, Type ownerType, StyledPropertyMetadata metadata, bool inherits = false, + Func validate = null, Action notifying = null) : base(name, ownerType, metadata, notifying) { @@ -38,6 +42,14 @@ namespace Avalonia } _inherits = inherits; + ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// @@ -59,6 +71,29 @@ namespace Avalonia /// public override bool Inherits => _inherits; + /// + /// Gets the value validation callback for the property. + /// + public Func ValidateValue { get; } + + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + + public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + /// /// Gets the default value for the property on the specified type. /// @@ -68,7 +103,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Typed; + return GetMetadata(type).DefaultValue; } /// @@ -120,57 +155,104 @@ namespace Avalonia /// The metadata. public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } /// - /// Overrides the validation function for the specified type. + /// Gets the string representation of the property. /// - /// The type. - /// The validation function. - public void OverrideValidation(Func validate) - where THost : IAvaloniaObject + /// The property's string representation. + public override string ToString() { - Func f; + return Name; + } - if (validate != null) - { - f = Cast(validate); - } - else + /// + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + /// + internal override void NotifyInitialized(IAvaloniaObject o) + { + if (HasNotifyInitializedObservers) { - // Passing null to the validation function means that the property metadata merge - // will take the base validation function, so instead use an empty validation. - f = (o, v) => v; + var e = new AvaloniaPropertyChangedEventArgs( + o, + this, + default, + o.GetValue(this), + BindingPriority.Unset); + NotifyInitialized(e); } + } - base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata(validate: f)); + /// + internal override void RouteClearValue(IAvaloniaObject o) + { + o.ClearValue(this); } - /// - /// Gets the string representation of the property. - /// - /// The property's string representation. - public override string ToString() + /// + internal override object RouteGetValue(IAvaloniaObject o) { - return Name; + return o.GetValue(this); } /// - Func IStyledPropertyAccessor.GetValidationFunc(Type type) + internal override void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) { - Contract.Requires(type != null); - return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate; + var v = TryConvert(value); + + if (v.HasValue) + { + o.SetValue(this, (TValue)v.Value, priority); + } + else if (v.Type == BindingValueType.UnsetValue) + { + o.ClearValue(this); + } + else if (v.HasError) + { + throw v.Error; + } } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + var adapter = TypedBindingAdapter.Create(o, this, source); + return o.Bind(this, adapter, priority); + } + + /// + internal override void RouteInheritanceParentChanged( + AvaloniaObject o, + IAvaloniaObject oldParent) + { + o.InheritanceParentChanged(this, oldParent); + } private object GetDefaultBoxedValue(Type type) { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue.Boxed; + return GetMetadata(type).DefaultValue; } [DebuggerHidden] diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index d1a0e2dc53..f96298a298 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -12,35 +12,35 @@ namespace Avalonia /// public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata { + private Optional _defaultValue; + /// /// Initializes a new instance of the class. /// /// The default value of the property. - /// A validation function. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( - TValue defaultValue = default, - Func validate = null, - BindingMode defaultBindingMode = BindingMode.Default) + Optional defaultValue = default, + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { - DefaultValue = new BoxedValue(defaultValue); - Validate = validate; + _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal BoxedValue DefaultValue { get; private set; } + public TValue DefaultValue => _defaultValue.GetValueOrDefault(); /// - /// Gets the validation callback. + /// Gets the value coercion callback, if any. /// - public Func Validate { get; private set; } - - object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; + public Func? CoerceValue { get; private set; } - Func IStyledPropertyMetadata.Validate => Cast(Validate); + object IStyledPropertyMetadata.DefaultValue => DefaultValue; /// public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property) @@ -49,29 +49,16 @@ namespace Avalonia if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue.Boxed == null) + if (!_defaultValue.HasValue) { - DefaultValue = src.DefaultValue; + _defaultValue = src.DefaultValue; } - if (Validate == null) + if (CoerceValue == null) { - Validate = src.Validate; + CoerceValue = src.CoerceValue; } } } - - [DebuggerHidden] - private static Func Cast(Func f) - { - if (f == null) - { - return null; - } - else - { - return (o, v) => f(o, (TValue)v); - } - } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index ac128d83de..a4e7a23454 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -129,6 +129,27 @@ namespace Avalonia.Utilities _entries[TryFindEntry(property.Id).Item1].Value = value; } + public void Remove(AvaloniaProperty property) + { + var (index, found) = TryFindEntry(property.Id); + + if (found) + { + Entry[] entries = new Entry[_entries.Length - 1]; + int ix = 0; + + for (int i = 0; i < _entries.Length; ++i) + { + if (i != index) + { + entries[ix++] = _entries[i]; + } + } + + _entries = entries; + } + } + public Dictionary ToDictionary() { var dict = new Dictionary(_entries.Length - 1); diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs deleted file mode 100644 index fe9b0e58a0..0000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ /dev/null @@ -1,128 +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; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetter - { - private readonly SingleOrQueue _pendingValues; - private bool _isNotifying; - - public DeferredSetter() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - public bool SetAndNotifyCallback(AvaloniaProperty property, ISetAndNotifyHandler setAndNotifyHandler, ref TValue backing, TValue value) - where TValue : TSetRecord - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetter _setter; - - internal NotifyDisposable(DeferredSetter setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } - - /// - /// Handler for set and notify requests. - /// - /// Value type. - internal interface ISetAndNotifyHandler - { - /// - /// Handles deferred setter requests to set a value. - /// - /// Property being set. - /// Backing field reference. - /// New value. - void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value); - } -} diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d85eb4cd76..d1393a9c0d 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -92,8 +92,7 @@ namespace Avalonia.Utilities /// True if the type accepts null values; otherwise false. public static bool AcceptsNull(Type type) { - var t = type.GetTypeInfo(); - return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>))); + return !type.IsValueType || IsNullableType(type); } /// @@ -119,10 +118,8 @@ namespace Avalonia.Utilities } var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (to.IsAssignableFrom(from)) { result = value; return true; @@ -134,7 +131,7 @@ namespace Avalonia.Utilities return true; } - if (toTypeInfo.IsEnum && from == typeof(string)) + if (to.IsEnum && from == typeof(string)) { if (Enum.IsDefined(to, (string)value)) { @@ -143,7 +140,7 @@ namespace Avalonia.Utilities } } - if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum) + if (!from.IsEnum && to.IsEnum) { result = null; @@ -154,7 +151,7 @@ namespace Avalonia.Utilities } } - if (fromTypeInfo.IsEnum && IsNumeric(to)) + if (from.IsEnum && IsNumeric(to)) { try { @@ -223,10 +220,8 @@ namespace Avalonia.Utilities } var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (to.IsAssignableFrom(from)) { result = value; return true; @@ -289,6 +284,17 @@ namespace Avalonia.Utilities return TryConvertImplicit(type, value, out object result) ? result : Default(type); } + public static T ConvertImplicit(object value) + { + if (TryConvertImplicit(typeof(T), value, out var result)) + { + return (T)result; + } + + throw new InvalidCastException( + $"Unable to convert object '{value ?? "(null)"}' of type '{value?.GetType()}' to type '{typeof(T)}'."); + } + /// /// Gets the default value for the specified type. /// @@ -296,9 +302,7 @@ namespace Avalonia.Utilities /// The default value. public static object Default(Type type) { - var typeInfo = type.GetTypeInfo(); - - if (typeInfo.IsValueType) + if (type.IsValueType) { return Activator.CreateInstance(type); } @@ -324,9 +328,11 @@ namespace Avalonia.Utilities return false; } - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + Type underlyingType = Nullable.GetUnderlyingType(type); + + if (underlyingType != null) { - return IsNumeric(Nullable.GetUnderlyingType(type)); + return IsNumeric(underlyingType); } else { @@ -341,6 +347,11 @@ namespace Avalonia.Utilities Explicit = 2 } + private static bool IsNullableType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType) { const string implicitName = "op_Implicit"; diff --git a/src/Avalonia.Base/Utilities/ValueSingleOrList.cs b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs new file mode 100644 index 0000000000..dc32cedb76 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// A list like struct optimized for holding zero or one items. + /// + /// The type of items held in the list. + /// + /// Once more than value has been added to this storage it will switch to using internally. + /// + public ref struct ValueSingleOrList + { + private bool _isSingleSet; + + /// + /// Single contained value. Only valid if is set. + /// + public T Single { get; private set; } + + /// + /// List of values. + /// + public List List { get; private set; } + + /// + /// If this struct is backed by a list. + /// + public bool HasList => List != null; + + /// + /// If this struct contains only single value and storage was not promoted to a list. + /// + public bool IsSingle => List == null && _isSingleSet; + + /// + /// Adds a value. + /// + /// Value to add. + public void Add(T value) + { + if (List != null) + { + List.Add(value); + } + else + { + if (!_isSingleSet) + { + Single = value; + + _isSingleSet = true; + } + else + { + List = new List(); + + List.Add(Single); + List.Add(value); + + Single = default; + } + } + } + + /// + /// Removes a value. + /// + /// Value to remove. + public bool Remove(T value) + { + if (List != null) + { + return List.Remove(value); + } + + if (!_isSingleSet) + { + return false; + } + + if (EqualityComparer.Default.Equals(Single, value)) + { + Single = default; + + _isSingleSet = false; + + return true; + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e06c5996c9..58ebc48652 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,205 +1,271 @@ using System; -using System.Collections.Generic; using Avalonia.Data; +using Avalonia.PropertyStore; using Avalonia.Utilities; +#nullable enable + namespace Avalonia { - internal class ValueStore : IPriorityValueOwner + /// + /// Stores styled property values for an . + /// + /// + /// At its core this class consists of an to + /// mapping which holds the current values for each set property. This + /// can be in one of 4 states: + /// + /// - For a single local value it will be an instance of . + /// - For a single value of a priority other than LocalValue it will be an instance of + /// ` + /// - For a single binding it will be an instance of + /// - For all other cases it will be an instance of + /// + internal class ValueStore : IValueSink { - private readonly AvaloniaPropertyValueStore _propertyValues; - private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; + private readonly IValueSink _sink; + private readonly AvaloniaPropertyValueStore _values; public ValueStore(AvaloniaObject owner) { - _owner = owner; - _propertyValues = new AvaloniaPropertyValueStore(); - _deferredSetters = new AvaloniaPropertyValueStore(); + _sink = _owner = owner; + _values = new AvaloniaPropertyValueStore(); } - public IDisposable AddBinding( - AvaloniaProperty property, - IObservable source, - BindingPriority priority) + public bool IsAnimating(AvaloniaProperty property) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) - { - priorityValue = v as PriorityValue; - - if (priorityValue == null) - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } - } - else + if (_values.TryGetValue(property, out var slot)) { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + return slot.ValuePriority < BindingPriority.LocalValue; } - return priorityValue.Add(source, (int)priority); + return false; } - public void AddValue(AvaloniaProperty property, object value, int priority) + public bool IsSet(AvaloniaProperty property) { - PriorityValue priorityValue; - - if (_propertyValues.TryGetValue(property, out var v)) + if (_values.TryGetValue(property, out var slot)) { - priorityValue = v as PriorityValue; - - if (priorityValue == null) - { - if (priority == (int)BindingPriority.LocalValue) - { - Validate(property, ref value); - _propertyValues.SetValue(property, value); - Changed(property, priority, v, value); - return; - } - else - { - priorityValue = CreatePriorityValue(property); - priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValue(property, priorityValue); - } - } + return slot.Value.HasValue; } - else + + return false; + } + + public bool TryGetValue(StyledPropertyBase property, out T value) + { + if (_values.TryGetValue(property, out var slot)) { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } + var v = (IValue)slot; - if (priority == (int)BindingPriority.LocalValue) - { - Validate(property, ref value); - _propertyValues.AddValue(property, value); - Changed(property, priority, AvaloniaProperty.UnsetValue, value); - return; - } - else + if (v.Value.HasValue) { - priorityValue = CreatePriorityValue(property); - _propertyValues.AddValue(property, priorityValue); + value = v.Value.Value; + return true; } } - priorityValue.SetValue(value, priority); + value = default!; + return false; } - public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) + public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) { - _owner.BindingNotificationReceived(property, notification); + if (property.ValidateValue?.Invoke(value) == false) + { + throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); + } + + if (_values.TryGetValue(property, out var slot)) + { + SetExisting(slot, property, value, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + _values.AddValue(property, entry); + entry.SetValue(value, priority); + } + else if (priority == BindingPriority.LocalValue) + { + _values.AddValue(property, new LocalValueEntry(value)); + _sink.ValueChanged(property, priority, default, value); + } + else + { + var entry = new ConstantValueEntry(property, value, priority); + _values.AddValue(property, entry); + _sink.ValueChanged(property, priority, default, value); + } } - public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) + public IDisposable AddBinding( + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - _owner.PriorityValueChanged(property, priority, oldValue, newValue); + if (_values.TryGetValue(property, out var slot)) + { + return BindExisting(slot, property, source, priority); + } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + var binding = entry.AddBinding(source, priority); + _values.AddValue(property, entry); + binding.Start(); + return binding; + } + else + { + var entry = new BindingEntry(_owner, property, source, priority, this); + _values.AddValue(property, entry); + entry.Start(); + return entry; + } } - public IDictionary GetSetValues() + public void ClearLocalValue(StyledPropertyBase property) { - return _propertyValues.ToDictionary(); + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.ClearLocalValue(); + } + else + { + var remove = slot is ConstantValueEntry c ? + c.Priority == BindingPriority.LocalValue : + !(slot is IPriorityValueEntry); + + if (remove) + { + var old = TryGetValue(property, out var value) ? value : default; + _values.Remove(property); + _sink.ValueChanged( + property, + BindingPriority.LocalValue, + old, + BindingValue.Unset); + } + } + } } - public void LogError(AvaloniaProperty property, Exception e) + public void CoerceValue(StyledPropertyBase property) { - _owner.LogBindingError(property, e); + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } } - public object GetValue(AvaloniaProperty property) + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { - var result = AvaloniaProperty.UnsetValue; - - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; + return new Diagnostics.AvaloniaPropertyValue( + property, + slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, + slot.ValuePriority, + null); } - return result; + return null; } - public bool IsAnimating(AvaloniaProperty property) + void IValueSink.ValueChanged( + StyledPropertyBase property, + BindingPriority priority, + Optional oldValue, + BindingValue newValue) { - return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + _sink.ValueChanged(property, priority, oldValue, newValue); } - public bool IsSet(AvaloniaProperty property) + void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { - if (_propertyValues.TryGetValue(property, out var value)) + if (_values.TryGetValue(property, out var slot)) { - return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; + if (slot == entry) + { + _values.Remove(property); + } } - - return false; } - public void Revalidate(AvaloniaProperty property) + private void SetExisting( + object slot, + StyledPropertyBase property, + T value, + BindingPriority priority) { - if (_propertyValues.TryGetValue(property, out var value)) + if (slot is IPriorityValueEntry e) { - (value as PriorityValue)?.Revalidate(); + var priorityValue = new PriorityValue(_owner, property, this, e); + _values.SetValue(property, priorityValue); + priorityValue.SetValue(value, priority); } - } - - public void VerifyAccess() => _owner.VerifyAccess(); - - private PriorityValue CreatePriorityValue(AvaloniaProperty property) - { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); - Func validate2 = null; - - if (validate != null) + else if (slot is PriorityValue p) { - validate2 = v => validate(_owner, v); + p.SetValue(value, priority); + } + else if (slot is LocalValueEntry l) + { + if (priority == BindingPriority.LocalValue) + { + var old = l.Value; + l.SetValue(value); + _sink.ValueChanged(property, priority, old, value); + } + else + { + var priorityValue = new PriorityValue(_owner, property, this, l); + _values.SetValue(property, priorityValue); + } + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - - return new PriorityValue( - this, - property, - property.PropertyType, - validate2); } - private void Validate(AvaloniaProperty property, ref object value) + private IDisposable BindExisting( + object slot, + StyledPropertyBase property, + IObservable> source, + BindingPriority priority) { - var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); + PriorityValue priorityValue; - if (validate != null && value != AvaloniaProperty.UnsetValue) + if (slot is IPriorityValueEntry e) { - value = validate(_owner, value); + priorityValue = new PriorityValue(_owner, property, this, e); } - } - - private DeferredSetter GetDeferredSetter(AvaloniaProperty property) - { - if (_deferredSetters.TryGetValue(property, out var deferredSetter)) + else if (slot is PriorityValue p) { - return (DeferredSetter)deferredSetter; + priorityValue = p; + } + else if (slot is LocalValueEntry l) + { + priorityValue = new PriorityValue(_owner, property, this, l); + } + else + { + throw new NotSupportedException("Unrecognised value store slot type."); } - var newDeferredSetter = new DeferredSetter(); - - _deferredSetters.AddValue(property, newDeferredSetter); - - return newDeferredSetter; - } - - public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); - } - - public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) - { - return GetDeferredSetter(property); + var binding = priorityValue.AddBinding(source, priority); + _values.SetValue(property, priorityValue); + binding.Start(); + return binding; } } } diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index 86113da87e..bea6f01243 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -238,7 +238,7 @@ namespace Avalonia.Collections } else { - return seq.ThenByDescending(o => GetValue(o), InternalComparer); + return seq.ThenBy(o => GetValue(o), InternalComparer); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index b65fd2a8b7..86133d5fdb 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -202,20 +202,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(ColumnHeaderHeight), defaultValue: double.NaN, - validate: ValidateColumnHeaderHeight); + validate: IsValidColumnHeaderHeight); - private static double ValidateColumnHeaderHeight(DataGrid grid, double value) + private static bool IsValidColumnHeaderHeight(double value) { - if (value < DATAGRID_minimumColumnHeaderHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness); } /// @@ -273,15 +265,7 @@ namespace Avalonia.Controls set { SetValue(FrozenColumnCountProperty, value); } } - private static int ValidateFrozenColumnCount(DataGrid grid, int value) - { - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; - } + private static bool ValidateFrozenColumnCount(int value) => value >= 0; public static readonly StyledProperty GridLinesVisibilityProperty = AvaloniaProperty.Register(nameof(GridLinesVisibility)); @@ -396,29 +380,11 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(MaxColumnWidth), defaultValue: DATAGRID_defaultMaxColumnWidth, - validate: ValidateMaxColumnWidth); + validate: IsValidColumnWidth); - private static double ValidateMaxColumnWidth(DataGrid grid, double value) + private static bool IsValidColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0); - } - if (grid.MinColumnWidth > value) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth)); - } - - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0); - } - - return value; + return !double.IsNaN(value) && value > 0; } /// @@ -434,28 +400,11 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(MinColumnWidth), defaultValue: DATAGRID_defaultMinColumnWidth, - validate: ValidateMinColumnWidth); + validate: IsValidMinColumnWidth); - private static double ValidateMinColumnWidth(DataGrid grid, double value) + private static bool IsValidMinColumnWidth(double value) { - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth)); - } - if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0); - } - if (double.IsPositiveInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth)); - } - if (grid.MaxColumnWidth < value) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth)); - } - - return value; + return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0; } /// @@ -483,19 +432,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(RowHeight), defaultValue: double.NaN, - validate: ValidateRowHeight); - private static double ValidateRowHeight(DataGrid grid, double value) + validate: IsValidRowHeight); + private static bool IsValidRowHeight(double value) { - if (value < DataGridRow.DATAGRIDROW_minimumHeight) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0); - } - if (value > DataGridRow.DATAGRIDROW_maximumHeight) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight); - } - - return value; + return double.IsNaN(value) || + (value >= DataGridRow.DATAGRIDROW_minimumHeight && + value <= DataGridRow.DATAGRIDROW_maximumHeight); } /// @@ -511,19 +453,12 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(RowHeaderWidth), defaultValue: double.NaN, - validate: ValidateRowHeaderWidth); - private static double ValidateRowHeaderWidth(DataGrid grid, double value) + validate: IsValidRowHeaderWidth); + private static bool IsValidRowHeaderWidth(double value) { - if (value < DATAGRID_minimumRowHeaderWidth) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth); - } - if (value > DATAGRID_maxHeadersThickness) - { - throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness); - } - - return value; + return double.IsNaN(value) || + (value >= DATAGRID_minimumRowHeaderWidth && + value <= DATAGRID_maxHeadersThickness); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 7dafef9d8b..69dfed761f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -68,25 +68,11 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(SublevelIndent), defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent, - validate: ValidateSublevelIndent); + validate: IsValidSublevelIndent); - private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value) + private static bool IsValidSublevelIndent(double value) { - // We don't need to revert to the old value if our input is bad because we never read this property value - if (double.IsNaN(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent)); - } - else if (double.IsInfinity(value)) - { - throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent)); - } - else if (value < 0) - { - throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0); - } - - return value; + return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0; } /// diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs index 060922238d..9feca71cda 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// public class DataGridFrozenGrid : Grid { - public static readonly AvaloniaProperty IsFrozenProperty = + public static readonly StyledProperty IsFrozenProperty = AvaloniaProperty.RegisterAttached("IsFrozen"); /// diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs index f15442addf..489bfc31d0 100644 --- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; -[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 64db832a81..6deddef0d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -378,7 +378,7 @@ namespace Avalonia.Controls public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( nameof(MinimumPrefixLength), 1, - validate: ValidateMinimumPrefixLength); + validate: IsValidMinimumPrefixLength); /// /// Identifies the @@ -392,7 +392,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(MinimumPopulateDelay), TimeSpan.Zero, - validate: ValidateMinimumPopulateDelay); + validate: IsValidMinimumPopulateDelay); /// /// Identifies the @@ -406,7 +406,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(MaxDropDownHeight), double.PositiveInfinity, - validate: ValidateMaxDropDownHeight); + validate: IsValidMaxDropDownHeight); /// /// Identifies the @@ -495,7 +495,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(FilterMode), defaultValue: AutoCompleteFilterMode.StartsWith, - validate: ValidateFilterMode); + validate: IsValidFilterMode); /// /// Identifies the @@ -546,26 +546,11 @@ namespace Avalonia.Controls o => o.AsyncPopulator, (o, v) => o.AsyncPopulator = v); - private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) - { - Contract.Requires(value >= -1); - - return value; - } - - private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value) - { - Contract.Requires(value.TotalMilliseconds >= 0.0); - - return value; - } + private static bool IsValidMinimumPrefixLength(int value) => value >= -1; - private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value) - { - Contract.Requires(value >= 0.0); + private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0; - return value; - } + private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0; private static bool IsValidFilterMode(AutoCompleteFilterMode mode) { @@ -590,12 +575,6 @@ namespace Avalonia.Controls return false; } } - private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value) - { - Contract.Requires(IsValidFilterMode(value)); - - return value; - } /// /// Handle the change of the IsEnabled property. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 78d02e200f..2e115463ac 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -306,18 +306,18 @@ namespace Avalonia.Controls } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.UpdateDataValidation(property, status); + base.UpdateDataValidation(property, value); if (property == CommandProperty) { - if (status?.ErrorType == BindingErrorType.Error) + if (value.Type == BindingValueType.BindingError) { if (_commandCanExecute) { diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index beafab3edf..94f8ad41a0 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -352,7 +352,8 @@ namespace Avalonia.Controls public static readonly StyledProperty DisplayModeProperty = AvaloniaProperty.Register( nameof(DisplayMode), - validate: ValidateDisplayMode); + validate: IsValidDisplayMode); + /// /// Gets or sets a value indicating whether the calendar is displayed in /// months, years, or decades. @@ -417,17 +418,6 @@ namespace Avalonia.Controls } OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode)); } - private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode) - { - if(IsValidDisplayMode(mode)) - { - return mode; - } - else - { - throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode"); - } - } private static bool IsValidDisplayMode(CalendarMode mode) { return mode == CalendarMode.Month diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 841b73cd92..b4d4fed9fc 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -190,13 +190,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(SelectedDateFormat), defaultValue: DatePickerFormat.Short, - validate: ValidateSelectedDateFormat); + validate: IsValidSelectedDateFormat); public static readonly StyledProperty CustomDateFormatStringProperty = AvaloniaProperty.Register( nameof(CustomDateFormatString), defaultValue: "d", - validate: ValidateDateFormatString); + validate: IsValidDateFormatString); public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( @@ -512,11 +512,17 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) { + base.OnPropertyChanged(property, oldValue, newValue, priority); + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, status.Error); + DataValidationErrors.SetError(this, newValue.Error); } } @@ -1140,27 +1146,9 @@ namespace Avalonia.Controls || value == DatePickerFormat.Short || value == DatePickerFormat.Custom; } - private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format) + private static bool IsValidDateFormatString(string formatString) { - if(IsValidSelectedDateFormat(format)) - { - return format; - } - else - { - throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid."); - } - } - private static string ValidateDateFormatString(DatePicker dp, string formatString) - { - if(string.IsNullOrWhiteSpace(formatString)) - { - throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString)); - } - else - { - return formatString; - } + return !string.IsNullOrWhiteSpace(formatString); } private static DateTime DiscardDayTime(DateTime d) { diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index e4ae777453..38ebbe5bf9 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -356,9 +356,13 @@ namespace Avalonia.Controls /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static string SharedSizeGroupPropertyValueValid(Control _, string value) + private static bool SharedSizeGroupPropertyValueValid(string value) { - Contract.Requires(value != null); + // null is default value + if (value == null) + { + return true; + } string id = (string)value; @@ -380,11 +384,11 @@ namespace Avalonia.Controls if (i == id.Length) { - return value; + return true; } } - throw new ArgumentException("Invalid SharedSizeGroup string."); + return false; } /// diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index b30a8668fd..ee27aa7ec1 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -1,9 +1,11 @@ -using Avalonia.Controls.Shapes; +using System; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Metadata; namespace Avalonia.Controls { + [Obsolete("Use Image control with DrawingImage source")] public class DrawingPresenter : Control { static DrawingPresenter() diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 23c1cd4794..1781067abb 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2741,11 +2741,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "Column", defaultValue: 0, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Column value."); - }); + validate: v => v >= 0); /// /// Row property. This is an attached property. @@ -2762,11 +2758,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "Row", defaultValue: 0, - validate: (_, v) => - { - if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Row value."); - }); + validate: v => v >= 0); /// /// ColumnSpan property. This is an attached property. @@ -2782,11 +2774,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "ColumnSpan", defaultValue: 1, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.ColumnSpan value."); - }); + validate: v => v >= 0); /// /// RowSpan property. This is an attached property. @@ -2802,11 +2790,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "RowSpan", defaultValue: 1, - validate: (_, v) => - { - if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.RowSpan value."); - }); + validate: v => v >= 0); /// /// IsSharedSizeScope property marks scoping element for shared size. diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index a2fefa0548..a2d53f4f06 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -23,37 +23,37 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeDirectionProperty = + public static readonly StyledProperty ResizeDirectionProperty = AvaloniaProperty.Register(nameof(ResizeDirection)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeBehaviorProperty = + public static readonly StyledProperty ResizeBehaviorProperty = AvaloniaProperty.Register(nameof(ResizeBehavior)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ShowsPreviewProperty = + public static readonly StyledProperty ShowsPreviewProperty = AvaloniaProperty.Register(nameof(ShowsPreview)); /// /// Defines the property. /// - public static readonly AvaloniaProperty KeyboardIncrementProperty = + public static readonly StyledProperty KeyboardIncrementProperty = AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); /// /// Defines the property. /// - public static readonly AvaloniaProperty DragIncrementProperty = + public static readonly StyledProperty DragIncrementProperty = AvaloniaProperty.Register(nameof(DragIncrement), 1d); /// /// Defines the property. /// - public static readonly AvaloniaProperty> PreviewContentProperty = + public static readonly StyledProperty> PreviewContentProperty = AvaloniaProperty.Register>(nameof(PreviewContent)); private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast); diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index ff6cd482df..41b6e5449a 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. @@ -23,6 +23,14 @@ namespace Avalonia.Controls public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchDirectionProperty = + AvaloniaProperty.Register( + nameof(StretchDirection), + StretchDirection.Both); + static Image() { AffectsRender(SourceProperty, StretchProperty); @@ -30,9 +38,9 @@ namespace Avalonia.Controls } /// - /// Gets or sets the bitmap image that will be displayed. + /// Gets or sets the image that will be displayed. /// - public IBitmap Source + public IImage Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -43,10 +51,19 @@ namespace Avalonia.Controls /// public Stretch Stretch { - get { return (Stretch)GetValue(StretchProperty); } + get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } + /// + /// Gets or sets a value controlling in what direction the image will be stretched. + /// + public StretchDirection StretchDirection + { + get { return GetValue(StretchDirectionProperty); } + set { SetValue(StretchDirectionProperty, value); } + } + /// /// Renders the control. /// @@ -58,8 +75,8 @@ namespace Avalonia.Controls if (source != null) { Rect viewPort = new Rect(Bounds.Size); - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize); + Size sourceSize = source.Size; + Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); Size scaledSize = sourceSize * scale; Rect destRect = viewPort .CenterRect(new Rect(scaledSize)) @@ -69,7 +86,7 @@ namespace Avalonia.Controls var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); - context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); + context.DrawImage(source, sourceRect, destRect, interpolationMode); } } @@ -85,15 +102,7 @@ namespace Avalonia.Controls if (source != null) { - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) - { - result = sourceSize; - } - else - { - result = Stretch.CalculateSize(availableSize, sourceSize); - } + result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection); } return result; @@ -106,7 +115,7 @@ namespace Avalonia.Controls if (source != null) { - var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var sourceSize = source.Size; var result = Stretch.CalculateSize(finalSize, sourceSize); return result; } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index db67a24159..a3bb654629 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -17,10 +17,10 @@ namespace Avalonia.Controls /// public class LayoutTransformControl : Decorator { - public static readonly AvaloniaProperty LayoutTransformProperty = + public static readonly StyledProperty LayoutTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); - public static readonly AvaloniaProperty UseRenderTransformProperty = + public static readonly StyledProperty UseRenderTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); static LayoutTransformControl() diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 4966e669ed..1761eeb3cf 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.InputModifiers & InputModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0); } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3ba0007f6b..e0baa5e679 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -26,8 +26,8 @@ namespace Avalonia.Controls /// public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, + menuItem => menuItem.Command, + (menuItem, command) => menuItem.Command = command, enableDataValidation: true); /// @@ -394,12 +394,12 @@ namespace Avalonia.Controls } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.UpdateDataValidation(property, status); + base.UpdateDataValidation(property, value); if (property == CommandProperty) { - if (status?.ErrorType == BindingErrorType.Error) + if (value.Type == BindingValueType.BindingError) { if (_commandCanExecute) { diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 5d3a4526cc..776e9d2171 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -52,13 +52,13 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu", validate: + = AvaloniaProperty.RegisterAttached("Menu"/*, validate: (o, v) => { if(!(o is Application || o is TopLevel)) throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); return v; - }); + }*/); public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7f69afaeeb..9e9eeec7c5 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -118,7 +118,7 @@ namespace Avalonia.Controls.Notifications /// Defines the CloseOnClick property. /// public static readonly AvaloniaProperty CloseOnClickProperty = - AvaloniaProperty.RegisterAttached("CloseOnClick", typeof(NotificationCard), validate: CloseOnClickChanged); + AvaloniaProperty.RegisterAttached("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/); private static bool CloseOnClickChanged(Button button, bool value) { diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 1a9347e317..f27bb5fac6 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Primitives; +using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -14,7 +15,7 @@ namespace Avalonia.Controls.Notifications /// /// An that displays notifications in a . /// - public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager + public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; @@ -153,5 +154,7 @@ namespace Avalonia.Controls.Notifications adornerLayer?.Children.Add(this); } + + public bool HitTest(Point point) => VisualChildren.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 6d450a0155..cbb5b667e7 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -58,7 +58,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d, validate: OnCoerceIncrement); + AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,13 +70,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -738,19 +738,34 @@ namespace Avalonia.Controls } } - private static double OnCoerceMaximum(NumericUpDown upDown, double value) + private static double OnCoerceMaximum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMaximum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMaximum(value); + } + + return value; } - private static double OnCoerceMinimum(NumericUpDown upDown, double value) + private static double OnCoerceMinimum(IAvaloniaObject instance, double value) { - return upDown.OnCoerceMinimum(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMinimum(value); + } + + return value; } - private static double OnCoerceIncrement(NumericUpDown upDown, double value) + private static double OnCoerceIncrement(IAvaloniaObject instance, double value) { - return upDown.OnCoerceIncrement(value); + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceIncrement(value); + } + + return value; } private void TextBoxOnTextChanged() diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 5e1a844720..9b1215c9ae 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly AvaloniaProperty BorderBrushProperty = + public static readonly StyledProperty BorderBrushProperty = Border.BorderBrushProperty.AddOwner(); /// diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index ebe5e0a93e..9a2f0310d7 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -138,10 +138,7 @@ namespace Avalonia.Controls.Primitives } } - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + public bool HitTest(Point point) => Children.HitTestCustom(point); private class AdornedElementInfo { diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 487a5e91e4..5150033a53 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -21,11 +21,8 @@ namespace Avalonia.Controls.Primitives return null; } - - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + + public bool HitTest(Point point) => Children.HitTestCustom(point); protected override Size ArrangeOverride(Size finalSize) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 3010a3d8a8..f0358ec04f 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -306,7 +306,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (placement == PlacementMode.Pointer) { positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); - positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; positionerParameters.Gravity = PopupPositioningEdge.BottomRight; } else diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 9251ca273f..4aaff94e44 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls.Primitives this.GetObservable(ViewportSizeProperty).Select(_ => Unit.Default), this.GetObservable(VisibilityProperty).Select(_ => Unit.Default)) .Select(_ => CalculateIsVisible()); - Bind(IsVisibleProperty, isVisible, BindingPriority.Style); + this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a5bbcec186..69da211aa4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -240,17 +241,14 @@ namespace Avalonia.Controls.Primitives public override void BeginInit() { base.BeginInit(); - ++_updateCount; - _updateSelectedIndex = int.MinValue; + + InternalBeginInit(); } /// public override void EndInit() { - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); base.EndInit(); } @@ -437,7 +435,8 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_updateCount; + + InternalBeginInit(); } /// @@ -445,10 +444,7 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextEndUpdate(); - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); } protected override void OnKeyDown(KeyEventArgs e) @@ -1118,6 +1114,26 @@ namespace Avalonia.Controls.Primitives } } + private void InternalBeginInit() + { + if (_updateCount == 0) + { + _updateSelectedIndex = int.MinValue; + } + + ++_updateCount; + } + + private void InternalEndInit() + { + Debug.Assert(_updateCount > 0); + + if (--_updateCount == 0) + { + UpdateFinished(); + } + } + private class Selection : IEnumerable { private readonly List _list = new List(); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index e7cd0697a0..4b3e8e2110 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -7,8 +7,14 @@ using Avalonia.Data; namespace Avalonia.Controls.Primitives { + /// + /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. + /// public class ToggleButton : Button { + /// + /// Defines the property. + /// public static readonly DirectProperty IsCheckedProperty = AvaloniaProperty.RegisterDirect( nameof(IsChecked), @@ -17,9 +23,30 @@ namespace Avalonia.Controls.Primitives unsetValue: null, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// public static readonly StyledProperty IsThreeStateProperty = AvaloniaProperty.Register(nameof(IsThreeState)); + /// + /// Defines the event. + /// + public static readonly RoutedEvent CheckedEvent = + RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent UncheckedEvent = + RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IndeterminateEvent = + RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble); + private bool? _isChecked = false; static ToggleButton() @@ -27,14 +54,49 @@ namespace Avalonia.Controls.Primitives PseudoClass(IsCheckedProperty, c => c == true, ":checked"); PseudoClass(IsCheckedProperty, c => c == false, ":unchecked"); PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate"); + + IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); + } + + /// + /// Raised when a is checked. + /// + public event EventHandler Checked + { + add => AddHandler(CheckedEvent, value); + remove => RemoveHandler(CheckedEvent, value); + } + + /// + /// Raised when a is unchecked. + /// + public event EventHandler Unchecked + { + add => AddHandler(UncheckedEvent, value); + remove => RemoveHandler(UncheckedEvent, value); + } + + /// + /// Raised when a is neither checked nor unchecked. + /// + public event EventHandler Indeterminate + { + add => AddHandler(IndeterminateEvent, value); + remove => RemoveHandler(IndeterminateEvent, value); } + /// + /// Gets or sets whether the is checked. + /// public bool? IsChecked { - get { return _isChecked; } - set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + get => _isChecked; + set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + /// + /// Gets or sets a value that indicates whether the control supports three states. + /// public bool IsThreeState { get => GetValue(IsThreeStateProperty); @@ -47,18 +109,78 @@ namespace Avalonia.Controls.Primitives base.OnClick(); } + /// + /// Toggles the property. + /// protected virtual void Toggle() { if (IsChecked.HasValue) + { if (IsChecked.Value) + { if (IsThreeState) + { IsChecked = null; + } else + { IsChecked = false; + } + } else + { IsChecked = true; + } + } else + { IsChecked = false; + } + } + + /// + /// Called when becomes true. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnChecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes false. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnUnchecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes null. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnIndeterminate(RoutedEventArgs e) + { + RaiseEvent(e); + } + + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool?)e.NewValue; + + switch (newValue) + { + case true: + OnChecked(new RoutedEventArgs(CheckedEvent)); + break; + case false: + OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + break; + default: + OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + break; + } } } } diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 539fe1ec4b..c7a1a24c25 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Remote Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride, new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen); } - context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), + context.DrawImage(_bitmap, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), new Rect(Bounds.Size)); } base.Render(context); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0e2136a6f3..086599d0bb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; @@ -21,7 +22,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + public static readonly StyledProperty HorizontalCacheLengthProperty = AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); /// @@ -39,16 +40,16 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty LayoutProperty = + public static readonly StyledProperty LayoutProperty = AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. /// - public static readonly AvaloniaProperty VerticalCacheLengthProperty = + public static readonly StyledProperty VerticalCacheLengthProperty = AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); - private static readonly AttachedProperty VirtualizationInfoProperty = + private static readonly StyledProperty VirtualizationInfoProperty = AvaloniaProperty.RegisterAttached("VirtualizationInfo"); internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); @@ -374,41 +375,37 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - var property = args.Property; - if (property == ItemsProperty) { - var newValue = (IEnumerable)args.NewValue; - var newDataSource = newValue as ItemsSourceView; - if (newValue != null && newDataSource == null) + var newEnumerable = newValue.GetValueOrDefault(); + var newDataSource = newEnumerable as ItemsSourceView; + if (newEnumerable != null && newDataSource == null) { - newDataSource = new ItemsSourceView(newValue); + newDataSource = new ItemsSourceView(newEnumerable); } OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); } else if (property == ItemTemplateProperty) { - OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == LayoutProperty) { - OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = (double)args.NewValue; + _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); } else if (property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = (double)args.NewValue; - } - else - { - base.OnPropertyChanged(args); + _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); } + + base.OnPropertyChanged(property, oldValue, newValue, priority); } internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) @@ -562,33 +559,35 @@ namespace Avalonia.Controls if (Layout != null) { - if (Layout is VirtualizingLayout virtualLayout) - { - var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + try + { _processingItemsSourceChange = args; - try + if (Layout is VirtualizingLayout virtualLayout) { virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } - finally - { - _processingItemsSourceChange = null; - } - } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) - { - // Walk through all the elements and make sure they are cleared for - // non-virtualizing layouts. - foreach (var element in Children) + else if (Layout is NonVirtualizingLayout nonVirtualLayout) { - if (GetVirtualizationInfo(element).IsRealized) + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) { - ClearElementImpl(element); + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } } + + Children.Clear(); } } + finally + { + _processingItemsSourceChange = null; + } InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 51c14d47d6..7d005a30b4 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -109,11 +109,22 @@ namespace Avalonia.Controls public void ClearElementToElementFactory(IControl element) { - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - _owner.ItemTemplateShim.RecycleElement(_owner, element); + if (_owner.ItemTemplateShim != null) + { + _owner.ItemTemplateShim.RecycleElement(_owner, element); + } + else + { + // No ItemTemplate to recycle to, remove the element from the children collection. + if (!_owner.Children.Remove(element)) + { + throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection."); + } + } + + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToElementFactory(); if (_lastFocusedElement == element) @@ -121,9 +132,8 @@ namespace Avalonia.Controls // Focused element is going away. Remove the tracked last focused element // and pick a reasonable next focus if we can find one within the layout // realized elements. - MoveFocusFromClearedIndex(clearedIndex); + MoveFocusFromClearedIndex(virtInfo.Index); } - } private void MoveFocusFromClearedIndex(int clearedIndex) @@ -190,7 +200,8 @@ namespace Avalonia.Controls { if (virtInfo == null) { - throw new ArgumentException("Element is not a child of this ItemsRepeater."); + //Element is not a child of this ItemsRepeater. + return -1; } return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; @@ -515,21 +526,52 @@ namespace Avalonia.Controls return element; } + // There are several cases handled here with respect to which element gets returned and when DataContext is modified. + // + // 1. If there is no ItemTemplate: + // 1.1 If data is an IControl -> the data is returned + // 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data + // + // 2. If there is an ItemTemplate: + // 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data + // 2.2 If data is an IControl: + // 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is + // 2.2.2 If Element returned by the ElementFactory is not the same as the data + // -> Element that is fetched from the ElementFactory is returned and + // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself private IControl GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. + var data = _owner.ItemsSourceView.GetAt(index); + var providedElementFactory = _owner.ItemTemplateShim; + + ItemTemplateWrapper GetElementFactory() + { + if (providedElementFactory == null) + { + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + return _owner.ItemTemplateShim; + } - var itemTemplateFactory = _owner.ItemTemplateShim; - if (itemTemplateFactory == null) + return providedElementFactory; + } + + IControl GetElement() { - // If no ItemTemplate was provided, use a default - var factory = FuncDataTemplate.Default; - _owner.ItemTemplate = factory; - itemTemplateFactory = _owner.ItemTemplateShim; + if (providedElementFactory == null) + { + if (data is IControl dataAsElement) + { + return dataAsElement; + } + } + + var elementFactory = GetElementFactory(); + return elementFactory.GetElement(_owner, data); } - var data = _owner.ItemsSourceView.GetAt(index); - var element = itemTemplateFactory.GetElement(_owner, data); + var element = GetElement(); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) @@ -537,8 +579,11 @@ namespace Avalonia.Controls virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } - // Prepare the element - element.DataContext = data; + if (data != element) + { + // Prepare the element + element.DataContext = data; + } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index cdf5010920..4fae867dbd 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -161,8 +161,6 @@ namespace Avalonia.Controls /// static ScrollViewer() { - AffectsValidation(ExtentProperty, OffsetProperty); - AffectsValidation(ViewportProperty, OffsetProperty); HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); VerticalScrollBarVisibilityProperty.Changed.AddClassHandler((x, e) => x.ScrollBarVisibilityChanged(e)); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3d472fca18..b561a3423f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); public static readonly DirectProperty TextProperty = - TextBlock.TextProperty.AddOwner( + TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, (o, v) => o.Text = v, defaultBindingMode: BindingMode.TwoWay, @@ -133,7 +133,7 @@ namespace Avalonia.Controls return ScrollBarVisibility.Hidden; } }); - Bind( + this.Bind( ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); @@ -390,8 +390,10 @@ namespace Avalonia.Controls { return; } + _undoRedoHelper.Snapshot(); HandleTextInput(text); + _undoRedoHelper.Snapshot(); } protected override void OnKeyDown(KeyEventArgs e) @@ -401,12 +403,12 @@ namespace Avalonia.Controls bool movement = false; bool selection = false; bool handled = false; - var modifiers = e.Modifiers; + var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - bool DetectSelection() => e.Modifiers.HasFlag(keymap.SelectionModifiers); + bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers); if (Match(keymap.SelectAll)) { @@ -700,11 +702,11 @@ namespace Avalonia.Controls } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { if (property == TextProperty) { - DataValidationErrors.SetError(this, status.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 738d9d0b51..6c8d58a8dd 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -406,7 +406,7 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.InputModifiers & InputModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0); } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 8081528e55..f9737b461d 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -47,7 +47,8 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])); + .Select(x => (x[0], x[1])) + .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index aa009770f6..00e68d629b 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -140,7 +140,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (e.Key == Key.LeftAlt) + if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt) { _altIsDown = true; @@ -218,6 +218,7 @@ namespace Avalonia.Input switch (e.Key) { case Key.LeftAlt: + case Key.RightAlt: _altIsDown = false; if (_ignoreAltUp) diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs index d39659cee3..723d577964 100644 --- a/src/Avalonia.Input/DragDrop.cs +++ b/src/Avalonia.Input/DragDrop.cs @@ -23,7 +23,7 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); - public static readonly AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); + public static readonly AttachedProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); /// /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 25d3b6887f..bcd962bc31 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; @@ -38,13 +38,13 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { var target = GetTarget(inputRoot, point); @@ -77,7 +77,7 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { try { @@ -100,16 +100,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragLeave: DragLeave(e.Root); break; case RawDragEventType.Drop: - e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index dc0b76b225..08e9eb7d33 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,8 +13,11 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; private set; } + public KeyModifiers KeyModifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -32,13 +35,25 @@ namespace Avalonia.Input return point; } + [Obsolete("Use constructor taking KeyModifiers")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { - this.Data = data; - this._target = target; - this._targetLocation = targetLocation; - this.Modifiers = modifiers; + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = modifiers; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF); + } + + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + : base(routedEvent) + { + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = (InputModifiers)keyModifiers; + KeyModifiers = keyModifiers; } } diff --git a/src/Avalonia.Input/GotFocusEventArgs.cs b/src/Avalonia.Input/GotFocusEventArgs.cs index 01e978a55c..8f9305e472 100644 --- a/src/Avalonia.Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Input/GotFocusEventArgs.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 Avalonia.Interactivity; namespace Avalonia.Input @@ -18,6 +19,16 @@ namespace Avalonia.Input /// /// Gets or sets any input modifiers active at the time of focus. /// - public InputModifiers InputModifiers { get; set; } + [Obsolete("Use KeyModifiers")] + public InputModifiers InputModifiers + { + get => (InputModifiers)KeyModifiers; + set => KeyModifiers = (KeyModifiers)((int)value & 0xF); + } + + /// + /// Gets or sets any key modifiers active at the time of focus. + /// + public KeyModifiers KeyModifiers { get; set; } } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f83c41e266..4dcee0bbc8 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -38,7 +38,9 @@ namespace Avalonia.Input /// The topmost at the specified position. public static IInputElement InputHitTest(this IInputElement element, Point p) { - return element.GetInputElementsAt(p).FirstOrDefault(); + Contract.Requires(element != null); + + return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement; } private static bool IsHitTestVisible(IVisual visual) diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs index a758a328be..053f894755 100644 --- a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -4,14 +4,14 @@ namespace Avalonia.Input.Platform { public class PlatformHotkeyConfiguration { - public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + public PlatformHotkeyConfiguration() : this(KeyModifiers.Control) { } - public PlatformHotkeyConfiguration(InputModifiers commandModifiers, - InputModifiers selectionModifiers = InputModifiers.Shift, - InputModifiers wholeWordTextActionModifiers = InputModifiers.Control) + public PlatformHotkeyConfiguration(KeyModifiers commandModifiers, + KeyModifiers selectionModifiers = KeyModifiers.Shift, + KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control) { CommandModifiers = commandModifiers; SelectionModifiers = selectionModifiers; @@ -75,9 +75,9 @@ namespace Avalonia.Input.Platform }; } - public InputModifiers CommandModifiers { get; set; } - public InputModifiers WholeWordTextActionModifiers { get; set; } - public InputModifiers SelectionModifiers { get; set; } + public KeyModifiers CommandModifiers { get; set; } + public KeyModifiers WholeWordTextActionModifiers { get; set; } + public KeyModifiers SelectionModifiers { get; set; } public List Copy { get; set; } public List Cut { get; set; } public List Paste { get; set; } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 5722e17593..4193cdafc5 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input.Raw +using System; + +namespace Avalonia.Input.Raw { public class RawDragEvent : RawInputEventArgs { @@ -6,7 +8,9 @@ public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; } + public KeyModifiers KeyModifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) @@ -17,6 +21,7 @@ Data = data; Effects = effects; Modifiers = (InputModifiers)modifiers; + KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); } } } diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..7f44c80a64 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -72,6 +72,7 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, string layoutId) { @@ -94,14 +95,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); } RaiseLineArranged(); @@ -115,10 +116,11 @@ namespace Avalonia.Layout public Size Arrange( Size finalSize, VirtualizingLayoutContext context, + bool isWrapping, LineAlignment lineAlignment, string layoutId) { - ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( Math.Max(finalSize.Width, _lastExtent.Width), @@ -270,6 +272,7 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, string layoutId) { if (anchorIndex != -1) @@ -280,7 +283,7 @@ namespace Avalonia.Layout var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; @@ -301,7 +304,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +342,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); @@ -544,6 +547,7 @@ namespace Avalonia.Layout private void ArrangeVirtualizingLayout( Size finalSize, LineAlignment lineAlignment, + bool isWrapping, string layoutId) { // Walk through the realized elements one line at a time and @@ -563,7 +567,7 @@ namespace Avalonia.Layout if (_orientation.MajorStart(currentBounds) != currentLineOffset) { spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); spaceAtLineStart = _orientation.MinorStart(currentBounds); countInLine = 0; currentLineOffset = _orientation.MajorStart(currentBounds); @@ -580,7 +584,7 @@ namespace Avalonia.Layout if (countInLine > 0) { var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); } } } @@ -594,6 +598,8 @@ namespace Avalonia.Layout double spaceAtLineEnd, double lineSize, LineAlignment lineAlignment, + bool isWrapping, + Size finalSize, string layoutId) { for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) @@ -659,6 +665,14 @@ namespace Avalonia.Layout } bounds = bounds.Translate(-_lastExtent.Position); + + if (!isWrapping) + { + _orientation.SetMinorSize( + ref bounds, + Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize))); + } + var element = _elementManager.GetAt(rangeIndex); element.Arrange(bounds); } diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index fba91e66c7..5d27ba9199 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -20,25 +20,25 @@ namespace Avalonia.Layout /// public sealed override void InitializeForContext(LayoutContext context) { - InitializeForContextCore((VirtualizingLayoutContext)context); + InitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override void UninitializeForContext(LayoutContext context) { - UninitializeForContextCore((VirtualizingLayoutContext)context); + UninitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override Size Measure(LayoutContext context, Size availableSize) { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize); } /// public sealed override Size Arrange(LayoutContext context, Size finalSize) { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize); } /// @@ -49,7 +49,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +61,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +83,7 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -98,6 +98,6 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; } } diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs new file mode 100644 index 0000000000..d3dec83e9b --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -0,0 +1,14 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +namespace Avalonia.Layout +{ + /// + /// Represents the base class for layout context types that do not support virtualization. + /// + public abstract class NonVirtualizingLayoutContext : LayoutContext + { + } +} diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31..e8ad49e9b9 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; namespace Avalonia.Layout { @@ -267,6 +268,7 @@ namespace Avalonia.Layout false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, LayoutId); @@ -278,6 +280,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + false, FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); @@ -293,11 +296,11 @@ namespace Avalonia.Layout InvalidateLayout(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - if (e.Property == OrientationProperty) + if (property == OrientationProperty) { - var orientation = (Orientation)e.NewValue; + var orientation = newValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index edc2042922..54c3ccbb90 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Data; namespace Avalonia.Layout { @@ -110,6 +111,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -123,6 +130,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -219,6 +227,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -269,15 +286,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -299,7 +318,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -329,17 +350,21 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) { _orientation.SetMinorSize( ref extent, - !double.IsInfinity(availableSizeMinor) ? + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? availableSizeMinor : - Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); _orientation.SetMajorSize( ref extent, Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); @@ -398,7 +423,7 @@ namespace Avalonia.Layout // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -406,6 +431,7 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, LayoutId); @@ -421,6 +447,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + true, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, LayoutId); return new Size(value.Width, value.Height); @@ -436,40 +463,45 @@ namespace Avalonia.Layout gridState.ClearElementOnDataSourceChange(context, args); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - if (args.Property == OrientationProperty) + if (property == OrientationProperty) { - var orientation = (Orientation)args.NewValue; + var orientation = newValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } - else if (args.Property == MinColumnSpacingProperty) + else if (property == MinColumnSpacingProperty) + { + _minColumnSpacing = newValue.GetValueOrDefault(); + } + else if (property == MinRowSpacingProperty) { - _minColumnSpacing = (double)args.NewValue; + _minRowSpacing = newValue.GetValueOrDefault(); } - else if (args.Property == MinRowSpacingProperty) + else if (property == ItemsJustificationProperty) { - _minRowSpacing = (double)args.NewValue; + _itemsJustification = newValue.GetValueOrDefault(); + ; } - else if (args.Property == ItemsJustificationProperty) + else if (property == ItemsStretchProperty) { - _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue; + _itemsStretch = newValue.GetValueOrDefault(); } - else if (args.Property == ItemsStretchProperty) + else if (property == MinItemWidthProperty) { - _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue; + _minItemWidth = newValue.GetValueOrDefault(); } - else if (args.Property == MinItemWidthProperty) + else if (property == MinItemHeightProperty) { - _minItemWidth = (double)args.NewValue; + _minItemHeight = newValue.GetValueOrDefault(); } - else if (args.Property == MinItemHeightProperty) + else if (property == MaximumRowsOrColumnsProperty) { - _minItemHeight = (double)args.NewValue; + _maximumRowsOrColumns = newValue.GetValueOrDefault(); } InvalidateLayout(); @@ -499,7 +531,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) { diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 65c2a75b1c..1a2bdeef1e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -19,8 +19,9 @@ - - + + + diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 571475c7ea..756619fa9f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -101,7 +101,7 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 67c383f6ae..1356dd58ff 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using SharpGen.Runtime; +using Avalonia.Platform; namespace Avalonia.Native { - public class CallbackBase : SharpGen.Runtime.IUnknown + public class CallbackBase : SharpGen.Runtime.IUnknown, IExceptionCallback { private uint _refCount; private bool _disposed; @@ -76,5 +78,15 @@ namespace Avalonia.Native { } + + public void RaiseException(Exception e) + { + if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) + { + threadingInterface.TerminateNativeApp(); + + threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); + } + } } } diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 353124a9d1..ba8cc83e83 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.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 System.Runtime.ExceptionServices; using System.Threading; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -43,6 +44,8 @@ namespace Avalonia.Native } readonly IAvnPlatformThreadingInterface _native; + private ExceptionDispatchInfo _exceptionDispatchInfo; + private CancellationTokenSource _exceptionCancellationSource; public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) { @@ -57,32 +60,49 @@ namespace Avalonia.Native public void RunLoop(CancellationToken cancellationToken) { - if (cancellationToken.CanBeCanceled == false) - _native.RunLoop(null); - else + _exceptionDispatchInfo?.Throw(); + var l = new object(); + _exceptionCancellationSource = new CancellationTokenSource(); + + var compositeCancellation = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; + + var cancellation = _native.CreateLoopCancellation(); + compositeCancellation.Register(() => { - var l = new object(); - var cancellation = _native.CreateLoopCancellation(); - cancellationToken.Register(() => + lock (l) { - lock (l) - { - cancellation?.Cancel(); - } - }); - try - { - _native.RunLoop(cancellation); + cancellation?.Cancel(); } - finally + }); + + try + { + _native.RunLoop(cancellation); + } + finally + { + lock (l) { - lock(l) - { - cancellation?.Dispose(); - cancellation = null; - } + cancellation?.Dispose(); + cancellation = null; } } + + if (_exceptionDispatchInfo != null) + { + _exceptionDispatchInfo.Throw(); + } + } + + public void DispatchException (ExceptionDispatchInfo exceptionInfo) + { + _exceptionDispatchInfo = exceptionInfo; + } + + public void TerminateNativeApp() + { + _exceptionCancellationSource?.Cancel(); } public void Signal(DispatcherPriority priority) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index e72fefe3ce..5d701dc8df 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -353,6 +353,11 @@ namespace Avalonia.Native public void SetCursor(IPlatformHandle cursor) { + if (_native == null) + { + return; + } + var newCursor = cursor as AvaloniaNativeCursor; newCursor = newCursor ?? (_cursorFactory.GetCursor(StandardCursorType.Arrow) as AvaloniaNativeCursor); _native.Cursor = newCursor.Cursor; diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 010acc3ae0..3a39beae94 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index f0f115afbc..10ae610345 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// @@ -41,4 +41,4 @@ namespace Avalonia.ReactiveUI set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index ac5db32c14..e111b15c75 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -58,7 +58,7 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty RouterProperty = + public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); /// @@ -118,4 +118,4 @@ namespace Avalonia.ReactiveUI Content = viewInstance; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 1bec5fc365..85768a39c3 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -16,14 +16,14 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty PageTransitionProperty = + public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. /// - public static readonly AvaloniaProperty DefaultContentProperty = + public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); /// @@ -72,4 +72,4 @@ namespace Avalonia.ReactiveUI await PageTransition.Start(null, this, true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Controls/ISetResourceParent.cs similarity index 61% rename from src/Avalonia.Styling/Styling/ISetStyleParent.cs rename to src/Avalonia.Styling/Controls/ISetResourceParent.cs index bca3d9d714..a1264adc34 100644 --- a/src/Avalonia.Styling/Styling/ISetStyleParent.cs +++ b/src/Avalonia.Styling/Controls/ISetResourceParent.cs @@ -1,29 +1,27 @@ -using Avalonia.Controls; - -namespace Avalonia.Styling +namespace Avalonia.Controls { /// - /// Defines an interface through which a 's parent can be set. + /// Defines an interface through which an 's parent can be set. /// /// /// You should not usually need to use this interface - it is for internal use only. /// - public interface ISetStyleParent : IStyle + public interface ISetResourceParent : IResourceNode { /// - /// Sets the style parent. + /// Sets the resource parent. /// /// The parent. void SetParent(IResourceNode parent); /// - /// Notifies the style that a change has been made to resources that apply to it. + /// Notifies the resource node that a change has been made to the resources in its parent. /// /// The event args. /// /// This method will be called automatically by the framework, you should not need to call /// this method yourself. /// - void NotifyResourcesChanged(ResourcesChangedEventArgs e); + void ParentResourcesChanged(ResourcesChangedEventArgs e); } } diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 901e27b7b7..acc2db1ff7 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -12,8 +12,12 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : AvaloniaDictionary, + IResourceDictionary, + IResourceNode, + ISetResourceParent { + private IResourceNode _parent; private AvaloniaList _mergedDictionaries; /// @@ -39,6 +43,12 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(this); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); @@ -48,11 +58,18 @@ namespace Avalonia.Controls }, x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(null); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); } + (x as ISetResourceParent)?.SetParent(null); x.ResourcesChanged -= MergedDictionaryResourcesChanged; }, () => { }); @@ -68,6 +85,27 @@ namespace Avalonia.Controls get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); } + /// + IResourceNode IResourceNode.ResourceParent => _parent; + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + NotifyMergedDictionariesResourcesChanged(e); + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceDictionary already has a parent."); + } + + _parent = parent; + } + /// public bool TryGetResource(object key, out object value) { @@ -95,7 +133,27 @@ namespace Avalonia.Controls ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); } - private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); + private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e) + { + if (_mergedDictionaries != null) + { + for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) + { + if (_mergedDictionaries[i] is ISetResourceParent merged) + { + merged.ParentResourcesChanged(e); + } + } + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var ev = new ResourcesChangedEventArgs(); + NotifyMergedDictionariesResourcesChanged(ev); + OnResourcesChanged(); + } + private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index f04f6fe9dc..13cfa905d4 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -223,13 +223,13 @@ namespace Avalonia { if (_styles != null) { - (_styles as ISetStyleParent)?.SetParent(null); + (_styles as ISetResourceParent)?.SetParent(null); _styles.ResourcesChanged -= ThisResourcesChanged; } _styles = value; - if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + if (value is ISetResourceParent setParent && setParent.ResourceParent == null) { setParent.SetParent(this); } @@ -471,7 +471,11 @@ namespace Avalonia OnAttachedToLogicalTreeCore(e); } - RaisePropertyChanged(ParentProperty, old, Parent, BindingPriority.LocalValue); + RaisePropertyChanged( + ParentProperty, + new Optional(old), + new BindingValue(Parent), + BindingPriority.LocalValue); } } diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index af209ea970..7d4e92baeb 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Utilities; namespace Avalonia.Styling { @@ -41,7 +43,8 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - var inputs = new List>(); + ValueSingleOrList> inputs = default; + var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -66,19 +69,25 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { + Debug.Assert(match.Activator != null); + inputs.Add(match.Activator); } selector = selector.MovePrevious(); } - if (inputs.Count > 0) + if (inputs.HasList) + { + return new SelectorMatch(StyleActivator.And(inputs.List)); + } + else if (inputs.IsSingle) { - return new SelectorMatch(StyleActivator.And(inputs)); + return new SelectorMatch(inputs.Single); } else { - return alwaysThisType && !hitCombinator ? + return alwaysThisType && !hitCombinator ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3ce82b4160..99ee8d8563 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, ISetStyleParent + public class Style : AvaloniaObject, IStyle, ISetResourceParent { private static Dictionary _applied = new Dictionary(); @@ -59,16 +59,16 @@ namespace Avalonia.Styling if (_resources != null) { - hadResources = _resources.Count > 0; + hadResources = _resources.HasResources; _resources.ResourcesChanged -= ResourceDictionaryChanged; } _resources = value; _resources.ResourcesChanged += ResourceDictionaryChanged; - if (hadResources || _resources.Count > 0) + if (hadResources || _resources.HasResources) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -194,13 +194,13 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index a4563110a9..0226288998 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { private IResourceNode _parent; private IResourceDictionary _resources; @@ -27,10 +27,10 @@ namespace Avalonia.Styling _styles.ForEachItem( x => { - if (x.ResourceParent == null && x is ISetStyleParent setParent) + if (x.ResourceParent == null && x is ISetResourceParent setParent) { setParent.SetParent(this); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -43,10 +43,10 @@ namespace Avalonia.Styling }, x => { - if (x.ResourceParent == this && x is ISetStyleParent setParent) + if (x.ResourceParent == this && x is ISetResourceParent setParent) { setParent.SetParent(null); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -98,7 +98,7 @@ namespace Avalonia.Styling if (hadResources || _resources.Count > 0) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -246,7 +246,7 @@ namespace Avalonia.Styling IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { @@ -257,7 +257,7 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } @@ -266,7 +266,7 @@ namespace Avalonia.Styling { foreach (var child in this) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } ResourcesChanged?.Invoke(this, e); @@ -280,7 +280,7 @@ namespace Avalonia.Styling { if (foundSource) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } foundSource |= child == sender; diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index a37463a0f0..3eb4f575c7 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Animation; using Avalonia.Animation.Animators; namespace Avalonia.Media @@ -11,7 +10,7 @@ namespace Avalonia.Media /// /// An ARGB color. /// - public readonly struct Color + public readonly struct Color : IEquatable { static Color() { @@ -19,22 +18,22 @@ namespace Avalonia.Media } /// - /// Gets or sets the Alpha component of the color. + /// Gets the Alpha component of the color. /// public byte A { get; } /// - /// Gets or sets the Red component of the color. + /// Gets the Red component of the color. /// public byte R { get; } /// - /// Gets or sets the Green component of the color. + /// Gets the Green component of the color. /// public byte G { get; } /// - /// Gets or sets the Blue component of the color. + /// Gets the Blue component of the color. /// public byte B { get; } @@ -144,5 +143,40 @@ namespace Avalonia.Media { return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + + /// + /// Check if two colors are equal. + /// + public bool Equals(Color other) + { + return A == other.A && R == other.R && G == other.G && B == other.B; + } + + public override bool Equals(object obj) + { + return obj is Color other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(Color left, Color right) + { + return left.Equals(right); + } + + public static bool operator !=(Color left, Color right) + { + return !left.Equals(right); + } } } diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 7784c73736..1e813edc13 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -14,13 +14,13 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty> DashesProperty = + public static readonly StyledProperty> DashesProperty = AvaloniaProperty.Register>(nameof(Dashes)); /// /// Defines the property. /// - public static readonly AvaloniaProperty OffsetProperty = + public static readonly StyledProperty OffsetProperty = AvaloniaProperty.Register(nameof(Offset)); private static ImmutableDashStyle s_dash; diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs index a60c591edc..6bc808e407 100644 --- a/src/Avalonia.Visuals/Media/Drawing.cs +++ b/src/Avalonia.Visuals/Media/Drawing.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media +using Avalonia.Platform; + +namespace Avalonia.Media { public abstract class Drawing : AvaloniaObject { @@ -6,4 +8,4 @@ public abstract Rect GetBounds(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index df69ab6fd5..4045b92c0c 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -74,18 +74,29 @@ namespace Avalonia.Media public Matrix CurrentContainerTransform => _currentContainerTransform; /// - /// Draws a bitmap image. + /// Draws an image. /// - /// The bitmap image. - /// The opacity to draw with. + /// The image. + /// The rect in the output to draw to. + public void DrawImage(IImage source, Rect rect) + { + Contract.Requires(source != null); + + DrawImage(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); - PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); + source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } /// diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index 744ff2af03..e581c8c553 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Media { @@ -55,4 +56,4 @@ namespace Avalonia.Media return rect; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs new file mode 100644 index 0000000000..57939bab24 --- /dev/null +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -0,0 +1,86 @@ +using System; +using Avalonia.Data; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// An that uses a for content. + /// + public class DrawingImage : AvaloniaObject, IImage, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or sets the drawing content. + /// + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + /// + public Size Size => Drawing?.GetBounds().Size ?? default; + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var drawing = Drawing; + + if (drawing == null) + { + return; + } + + var bounds = drawing.GetBounds(); + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y); + + using (context.PushClip(destRect)) + using (context.PushPreTransform(translate * scale)) + { + Drawing?.Draw(context); + } + } + + /// + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + base.OnPropertyChanged(property, oldValue, newValue, priority); + + if (property == DrawingProperty) + { + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index 3dad10fb8f..4df3aa8ae2 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -1,10 +1,13 @@ -namespace Avalonia.Media +using Avalonia.Metadata; + +namespace Avalonia.Media { public class GeometryDrawing : Drawing { public static readonly StyledProperty GeometryProperty = AvaloniaProperty.Register(nameof(Geometry)); + [Content] public Geometry Geometry { get => GetValue(GeometryProperty); diff --git a/src/Avalonia.Visuals/Media/IImage.cs b/src/Avalonia.Visuals/Media/IImage.cs new file mode 100644 index 0000000000..aff2a9ddf9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IImage.cs @@ -0,0 +1,29 @@ +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Represents a raster or vector image. + /// + public interface IImage + { + /// + /// Gets the size of the image, in device independent pixels. + /// + Size Size { get; } + + /// + /// Draws the image to a . + /// + /// The drawing context. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode); + } +} diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 8dd75d2374..14ac4261dc 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Imaging { @@ -94,9 +95,28 @@ namespace Avalonia.Media.Imaging PlatformImpl.Item.Save(fileName); } + /// + /// Saves the bitmap to a stream. + /// + /// The stream. public void Save(Stream stream) { PlatformImpl.Item.Save(stream); } + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + context.PlatformImpl.DrawBitmap( + PlatformImpl, + 1, + sourceRect, + destRect, + bitmapInterpolationMode); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs index 90b13088e1..4c3203a95b 100644 --- a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.Imaging /// /// Represents a bitmap image. /// - public interface IBitmap : IDisposable + public interface IBitmap : IImage, IDisposable { /// /// Gets the dots per inch (DPI) of the image. @@ -32,15 +32,6 @@ namespace Avalonia.Media.Imaging /// IRef PlatformImpl { get; } - /// - /// Gets the size of the image, in device independent pixels. - /// - /// - /// Note that Skia does not currently support reading the DPI of an image so this value - /// will equal on Skia. - /// - Size Size { get; } - /// /// Saves the bitmap to a file. /// diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index b55ca251a6..7d3f3bc4eb 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -1,12 +1,14 @@ // 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.Immutable { /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush + public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -47,6 +49,35 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + public bool Equals(ImmutableSolidColorBrush other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return Color == other.Color && Opacity == other.Opacity; + } + + public override bool Equals(object obj) + { + return obj is ImmutableSolidColorBrush other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode(); + } + } + + public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return !left.Equals(right); + } + /// /// Returns a string representation of the brush. /// diff --git a/src/Avalonia.Visuals/Media/MediaExtensions.cs b/src/Avalonia.Visuals/Media/MediaExtensions.cs index 95d17b454e..36bda5f483 100644 --- a/src/Avalonia.Visuals/Media/MediaExtensions.cs +++ b/src/Avalonia.Visuals/Media/MediaExtensions.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.Utilities; namespace Avalonia.Media { @@ -16,24 +17,82 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// A vector with the X and Y scaling factors. - public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Vector CalculateScaling( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - double scaleX = 1; - double scaleY = 1; + var scaleX = 1.0; + var scaleY = 1.0; - if (stretch != Stretch.None) + bool isConstrainedWidth = !double.IsPositiveInfinity(destinationSize.Width); + bool isConstrainedHeight = !double.IsPositiveInfinity(destinationSize.Height); + + if ((stretch == Stretch.Uniform || stretch == Stretch.UniformToFill || stretch == Stretch.Fill) + && (isConstrainedWidth || isConstrainedHeight)) { - scaleX = destinationSize.Width / sourceSize.Width; - scaleY = destinationSize.Height / sourceSize.Height; + // Compute scaling factors for both axes + scaleX = MathUtilities.IsZero(sourceSize.Width) ? 0.0 : destinationSize.Width / sourceSize.Width; + scaleY = MathUtilities.IsZero(sourceSize.Height) ? 0.0 : destinationSize.Height / sourceSize.Height; - switch (stretch) + if (!isConstrainedWidth) + { + scaleX = scaleY; + } + else if (!isConstrainedHeight) + { + scaleY = scaleX; + } + else { - case Stretch.Uniform: - scaleX = scaleY = Math.Min(scaleX, scaleY); + // If not preserving aspect ratio, then just apply transform to fit + switch (stretch) + { + case Stretch.Uniform: + // Find minimum scale that we use for both axes + double minscale = scaleX < scaleY ? scaleX : scaleY; + scaleX = scaleY = minscale; + break; + + case Stretch.UniformToFill: + // Find maximum scale that we use for both axes + double maxscale = scaleX > scaleY ? scaleX : scaleY; + scaleX = scaleY = maxscale; + break; + + case Stretch.Fill: + // We already computed the fill scale factors above, so just use them + break; + } + } + + // Apply stretch direction by bounding scales. + // In the uniform case, scaleX=scaleY, so this sort of clamping will maintain aspect ratio + // In the uniform fill case, we have the same result too. + // In the fill case, note that we change aspect ratio, but that is okay + switch (stretchDirection) + { + case StretchDirection.UpOnly: + if (scaleX < 1.0) + scaleX = 1.0; + if (scaleY < 1.0) + scaleY = 1.0; + break; + + case StretchDirection.DownOnly: + if (scaleX > 1.0) + scaleX = 1.0; + if (scaleY > 1.0) + scaleY = 1.0; break; - case Stretch.UniformToFill: - scaleX = scaleY = Math.Max(scaleX, scaleY); + + case StretchDirection.Both: + break; + + default: break; } } @@ -47,10 +106,15 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// The size of the stretched source. - public static Size CalculateSize(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Size CalculateSize( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize); + return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize, stretchDirection); } } } diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 5ed16ca957..0fdc40c85c 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -23,7 +23,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty IsFilledProperty = + public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); private Points _points; diff --git a/src/Avalonia.Visuals/Media/StretchDirection.cs b/src/Avalonia.Visuals/Media/StretchDirection.cs new file mode 100644 index 0000000000..a4be26f6cd --- /dev/null +++ b/src/Avalonia.Visuals/Media/StretchDirection.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Media +{ + /// + /// Describes the type of scaling that can be used when scaling content. + /// + public enum StretchDirection + { + /// + /// Only scales the content upwards when the content is smaller than the available space. + /// If the content is larger, no scaling downwards is done. + /// + UpOnly, + + /// + /// Only scales the content downwards when the content is larger than the available space. + /// If the content is smaller, no scaling upwards is done. + /// + DownOnly, + + /// + /// Always stretches to fit the available space according to the stretch mode. + /// + Both, + } +} diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 3a47f40045..886a6479dd 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty ChildrenProperty = + public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); public TransformGroup() diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index f2309c271d..7d142b0759 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -33,7 +33,7 @@ namespace Avalonia.Platform /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 2fa249f101..b065079564 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -160,16 +160,23 @@ namespace Avalonia.Rendering /// public IEnumerable HitTest(Point p, IVisual root, Func filter) { - if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) - { - // When unit testing the renderLoop may be null, so update the scene manually. - UpdateScene(); - } + EnsureCanHitTest(); + //It's safe to access _scene here without a lock since //it's only changed from UI thread which we are currently on return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); } + /// + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + EnsureCanHitTest(); + + //It's safe to access _scene here without a lock since + //it's only changed from UI thread which we are currently on + return _scene?.Item.HitTestFirst(p, root, filter); + } + /// public void Paint(Rect rect) { @@ -235,6 +242,15 @@ namespace Avalonia.Rendering internal Scene UnitTestScene() => _scene.Item; + private void EnsureCanHitTest() + { + if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) + { + // When unit testing the renderLoop may be null, so update the scene manually. + UpdateScene(); + } + } + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) @@ -469,11 +485,11 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); + context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { - context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); + context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } if (layer.GeometryClip != null) @@ -485,7 +501,7 @@ namespace Avalonia.Rendering if (_overlay != null) { var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); - context.DrawImage(_overlay, 0.5, sourceRect, clientRect); + context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); } if (DrawFps) diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs index 7199053b08..4c15de0312 100644 --- a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs +++ b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + namespace Avalonia.Rendering { /// @@ -9,4 +13,13 @@ namespace Avalonia.Rendering { bool HitTest(Point point); } + + public static class CustomSimpleHitTestExtensions + { + public static bool HitTestCustom(this IVisual visual, Point point) + => (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; + + public static bool HitTestCustom(this IEnumerable children, Point point) + => children.Any(ctrl => ctrl.HitTestCustom(point)); + } } diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 9ad7186dca..7dc2dfde88 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -50,6 +50,18 @@ namespace Avalonia.Rendering /// The visuals at the specified point, topmost first. IEnumerable HitTest(Point p, IVisual root, Func filter); + /// + /// Hit tests a location to find first visual at the specified point. + /// + /// The point, in client coordinates. + /// The root of the subtree to search. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the specified point, topmost first. + IVisual HitTestFirst(Point p, IVisual root, Func filter); + /// /// Informs the renderer that the z-ordering of a visual's children has changed. /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 68d56eeedd..aade57b7f0 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -164,6 +164,11 @@ namespace Avalonia.Rendering return HitTest(root, p, filter); } + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + return HitTest(root, p, filter).FirstOrDefault(); + } + /// public void RecalculateChildren(IVisual visual) => AddDirty(visual); @@ -307,7 +312,9 @@ namespace Avalonia.Rendering if (!child.ClipToBounds || clipRect.Intersects(childBounds)) { - var childClipRect = clipRect.Translate(-childBounds.Position); + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; Render(context, child, childClipRect); } else diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index a169a629be..b362321745 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -115,7 +115,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var next = NextDrawAs(); @@ -130,7 +130,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) { // This method is currently only used to composite layers so shouldn't be called here. throw new NotSupportedException(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index e1bdcaab3b..054f33c95d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 1afc096c98..70355e6833 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -2,8 +2,10 @@ // 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 System.Linq; +using Avalonia.Collections.Pooled; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -128,7 +130,20 @@ namespace Avalonia.Rendering.SceneGraph public IEnumerable HitTest(Point p, IVisual root, Func filter) { var node = FindNode(root); - return (node != null) ? HitTest(node, p, null, filter) : Enumerable.Empty(); + return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty(); + } + + /// + /// Gets the visual at a point in the scene. + /// + /// The point. + /// The root of the subtree to search. + /// A filter. May be null. + /// The visual at the specified point. + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + var node = FindNode(root); + return (node != null) ? HitTestFirst(node, p, filter) : null; } /// @@ -158,38 +173,157 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private IEnumerable HitTest(IVisualNode node, Point p, Rect? clip, Func filter) + private IVisual HitTestFirst(IVisualNode root, Point p, Func filter) { - if (filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + using var enumerator = new HitTestEnumerator(root, filter, p, Root); + + enumerator.MoveNext(); + + return enumerator.Current; + } + + private class HitTestEnumerable : IEnumerable + { + private readonly IVisualNode _root; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private readonly Point _point; + + public HitTestEnumerable(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) { - var clipped = false; + _root = root; + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; + } - if (node.ClipToBounds) - { - clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); - clipped = !clip.Value.Contains(p); - } + public IEnumerator GetEnumerator() + { + return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); + } - if (node.GeometryClip != null) - { - var controlPoint = Root.Visual.TranslatePoint(p, node.Visual); - clipped = !node.GeometryClip.FillContains(controlPoint.Value); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private struct HitTestEnumerator : IEnumerator + { + private readonly PooledStack _nodeStack; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private IVisual _current; + private readonly Point _point; + + public HitTestEnumerator(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) + { + _nodeStack = new PooledStack(); + _nodeStack.Push(new Entry(root, false, null, true)); + + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; - if (!clipped) + _current = null; + } + + public bool MoveNext() + { + while (_nodeStack.Count > 0) { - for (var i = node.Children.Count - 1; i >= 0; --i) + (var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); + + if (wasVisited && isRoot) { - foreach (var h in HitTest(node.Children[i], p, clip, filter)) + break; + } + + var children = node.Children; + int childCount = children.Count; + + if (childCount == 0 || wasVisited) + { + if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point)) { - yield return h; + _current = node.Visual; + + return true; } } + else if (FilterAndClip(node, ref clip)) + { + _nodeStack.Push(new Entry(node, true, null)); + + for (var i = 0; i < childCount; i++) + { + _nodeStack.Push(new Entry(children[i], false, clip)); + } + } + } + + return false; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public IVisual Current => _current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _nodeStack.Dispose(); + } - if (node.HitTest(p)) + private bool FilterAndClip(IVisualNode node, ref Rect? clip) + { + if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + { + var clipped = false; + + if (node.ClipToBounds) { - yield return node.Visual; + clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); + clipped = !clip.Value.Contains(_point); } + + if (node.GeometryClip != null) + { + var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); + clipped = !node.GeometryClip.FillContains(controlPoint.Value); + } + + return !clipped; + } + + return false; + } + + private readonly struct Entry + { + public readonly bool WasVisited; + public readonly bool IsRoot; + public readonly IVisualNode Node; + public readonly Rect? Clip; + + public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) + { + Node = node; + WasVisited = wasVisited; + IsRoot = isRoot; + Clip = clip; + } + + public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) + { + wasVisited = WasVisited; + isRoot = IsRoot; + node = Node; + clip = Clip; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index d2a9e0a673..93d68e5230 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -280,8 +280,13 @@ namespace Avalonia.Rendering.SceneGraph /// public bool HitTest(Point p) { - foreach (var operation in DrawOperations) + var drawOperations = DrawOperations; + var drawOperationsCount = drawOperations.Count; + + for (var i = 0; i < drawOperationsCount; i++) { + var operation = drawOperations[i]; + if (operation?.Item?.HitTest(p) == true) { return true; diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 465b3a65be..ef7a254f86 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -447,7 +447,11 @@ namespace Avalonia /// The new visual parent. protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged( + VisualParentProperty, + new Optional(oldParent), + new BindingValue(newParent), + BindingPriority.LocalValue); } protected override sealed void LogBindingError(AvaloniaProperty property, Exception e) diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 8e3c7e0765..f32d67db1e 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -213,12 +213,37 @@ namespace Avalonia.VisualTree /// /// The root visual to test. /// The point. - /// The visuals at the requested point. + /// The visual at the requested point. public static IVisual GetVisualAt(this IVisual visual, Point p) { Contract.Requires(visual != null); - return visual.GetVisualsAt(p).FirstOrDefault(); + return visual.GetVisualAt(p, x => x.IsVisible); + } + + /// + /// Gets the first visual in the visual tree whose bounds contain a point. + /// + /// The root visual to test. + /// The point. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the requested point. + public static IVisual GetVisualAt(this IVisual visual, Point p, Func filter) + { + Contract.Requires(visual != null); + + var root = visual.GetVisualRoot(); + var rootPoint = visual.TranslatePoint(p, root); + + if (rootPoint.HasValue) + { + return root.Renderer.HitTestFirst(rootPoint.Value, visual, filter); + } + + return null; } /// diff --git a/src/Avalonia.X11/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index c3a2fd2050..714a592f2b 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -84,8 +84,24 @@ namespace Avalonia.X11.Glx [GlEntryPoint("glGetError")] public GlGetError GetError { get; } - public GlxInterface() : base(GlxGetProcAddress) + public GlxInterface() : base(SafeGetProcAddress) { } + + // Ignores egl functions. + // On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions. + // This makes Skia try to load some data from EGL, + // which can then cause segmentation faults because they return garbage. + public static IntPtr SafeGetProcAddress(string proc, bool optional) + { + if (proc.StartsWith("egl", StringComparison.InvariantCulture)) + { + return IntPtr.Zero; + } + + return GlxConverted(proc, optional); + } + + private static readonly Func GlxConverted = ConvertNative(GlxGetProcAddress); } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 04f2a7137c..22eb0792e8 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11.Glx ImmediateContext.MakeCurrent(); var err = Glx.GetError(); - GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); + GlInterface = new GlInterface(GlxInterface.SafeGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); if (GlInterface.Renderer == null) diff --git a/src/Avalonia.X11/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs index 00288f300d..94f930e9ec 100644 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ b/src/Avalonia.X11/X11Framebuffer.cs @@ -14,6 +14,10 @@ namespace Avalonia.X11 public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) { + // HACK! Please fix renderer, should never ask for 0x0 bitmap. + width = Math.Max(1, width); + height = Math.Max(1, height); + _display = display; _xid = xid; _depth = depth; diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index f0e75536d0..093f2b12c1 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -59,7 +59,7 @@ namespace Avalonia.X11 } using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) - ctx.DrawImage(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), + ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 6ba562bb69..8b531bd9c5 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -44,7 +44,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Control)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 6ff5b96f12..919abae243 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -102,7 +102,7 @@ namespace Avalonia.X11 valueMask |= SetWindowValuemask.ColorMap; } - int defaultWidth = 300, defaultHeight = 200; + int defaultWidth = 0, defaultHeight = 0; if (!_popup && Screen != null) { @@ -117,6 +117,10 @@ namespace Avalonia.X11 } } + // check if the calculated size is zero then compensate to hardcoded resolution + defaultWidth = Math.Max(defaultWidth, 300); + defaultHeight = Math.Max(defaultHeight, 200); + _handle = XCreateWindow(_x11.Display, _x11.RootWindow, 10, 10, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, @@ -133,7 +137,7 @@ namespace Avalonia.X11 _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); - _realSize = new PixelSize(300, 200); + _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 20f68df820..c77ccd64f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions StringFormat = StringFormat, RelativeSource = RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + TargetNullValue = TargetNullValue, NameScope = new WeakReference(serviceProvider.GetService()) }; } @@ -86,5 +87,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string StringFormat { get; set; } public RelativeSource RelativeSource { get; set; } + + public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 3525628a79..0d56942645 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude :IResourceProvider + public class ResourceInclude : IResourceNode, ISetResourceParent { + private IResourceNode _parent; private Uri _baseUri; private IResourceDictionary _loaded; @@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + (_loaded as ISetResourceParent)?.SetParent(this); + _loaded.ResourcesChanged += ResourcesChanged; + if (_loaded.HasResources) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); @@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// bool IResourceProvider.HasResources => Loaded.HasResources; + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// bool IResourceProvider.TryGetResource(object key, out object value) { return Loaded.TryGetResource(key, out value); } + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceInclude already has a parent."); + } + + _parent = parent; + } + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + (_loaded as ISetResourceParent)?.ParentResourcesChanged(e); + } + public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { var tdc = (ITypeDescriptorContext)serviceProvider; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 7acee50d80..41eab79ed8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle, ISetStyleParent + public class StyleInclude : IStyle, ISetResourceParent { private Uri _baseUri; private IStyle _loaded; @@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetStyleParent)?.SetParent(this); + (_loaded as ISetResourceParent)?.SetParent(this); } return _loaded; @@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { - (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + (Loaded as ISetResourceParent)?.ParentResourcesChanged(e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 63c8b1c074..ebe4035ed6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -99,7 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); - Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 8a5a725594..a23c31ca64 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -75,7 +75,7 @@ namespace Avalonia.Shared.PlatformSupport lock (_btlock) Backtraces.Remove(_backtrace); #endif - _plat.Free(_address, Size); + _plat?.Free(_address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true; _address = IntPtr.Zero; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d06cfa69a7..1c05f8ac9f 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); @@ -146,10 +146,10 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); - DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); + DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); } @@ -437,7 +437,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage( + context.DrawBitmap( RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, sourceRect, diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 727947e59d..60d6ecaabc 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -89,12 +89,15 @@ namespace Avalonia.Skia if (typeface.FontFamily.Key == null) { + var defaultName = SKTypeface.Default.FamilyName; + foreach (var familyName in typeface.FontFamily.FamilyNames) { skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); - if (skTypeface == SKTypeface.Default) + if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) && + defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) { continue; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index aa13003643..81d869f3b8 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) { @@ -149,7 +149,7 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index fbc6d21cb7..6632e2b3e7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -107,7 +107,7 @@ namespace Avalonia.Direct2D1.Media context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); + context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 1ee869ecb9..29c9280af6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.Media _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); } - public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector(); + public override Vector Dpi => new Vector(96, 96); public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia(); public override void Dispose() @@ -58,3 +58,4 @@ namespace Avalonia.Direct2D1.Media } } } +; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 8ec368c999..b96441e357 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -58,7 +58,7 @@ namespace Avalonia.Direct2D1.Media.Imaging { using (var dc = wic.CreateDrawingContext(null)) { - dc.DrawImage( + dc.DrawBitmap( RefCountable.CreateUnownedNotClonable(this), 1, new Rect(PixelSize.ToSizeWithDpi(Dpi.X)), diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 176c3e0e23..4e0853c20c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -26,6 +26,7 @@ namespace Avalonia.Direct2D1.Media using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + Dpi = new Vector(96, 96); } } @@ -39,6 +40,7 @@ namespace Avalonia.Direct2D1.Media _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + Dpi = new Vector(96, 96); } /// @@ -62,6 +64,7 @@ namespace Avalonia.Direct2D1.Media pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); WicImpl.SetResolution(dpi.X, dpi.Y); + Dpi = dpi; } public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) @@ -70,6 +73,8 @@ namespace Avalonia.Direct2D1.Media WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + Dpi = dpi; + using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { for (var row = 0; row < size.Height; row++) @@ -82,14 +87,7 @@ namespace Avalonia.Direct2D1.Media } } - public override Vector Dpi - { - get - { - WicImpl.GetResolution(out double x, out double y); - return new Vector(x, y); - } - } + public override Vector Dpi { get; } public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index 11c0a6dca9..b38c09c07a 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; -using System.Text; using Avalonia.Input; using Avalonia.Win32.Interop; @@ -211,7 +210,7 @@ namespace Avalonia.Win32.Input { 31, Key.ImeModeChange }, { 32, Key.Space }, { 33, Key.PageUp }, - { 34, Key.Next }, + { 34, Key.PageDown }, { 35, Key.End }, { 36, Key.Home }, { 37, Key.Left }, @@ -364,17 +363,80 @@ namespace Avalonia.Win32.Input { 254, Key.OemClear }, }; - public static Key KeyFromVirtualKey(int virtualKey) + /// + /// Indicates whether the key is an extended key, such as the right-hand ALT and CTRL keys. + /// According to https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown. + /// + private static bool IsExtended(int keyData) { - Key result; - s_keyFromVirtualKey.TryGetValue(virtualKey, out result); + const int extendedMask = 1 << 24; + + return (keyData & extendedMask) != 0; + } + + private static int GetVirtualKey(int virtualKey, int keyData) + { + // Adapted from https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndKeyboardInputProvider.cs. + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_SHIFT) + { + // Bits from 16 to 23 represent scan code. + const int scanCodeMask = 0xFF0000; + + var scanCode = (keyData & scanCodeMask) >> 16; + + virtualKey = (int)UnmanagedMethods.MapVirtualKey((uint)scanCode, (uint)UnmanagedMethods.MapVirtualKeyMapTypes.MAPVK_VSC_TO_VK_EX); + + if (virtualKey == 0) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LSHIFT; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_MENU) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RMENU; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LMENU; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_CONTROL) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RCONTROL; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LCONTROL; + } + } + + return virtualKey; + } + + public static Key KeyFromVirtualKey(int virtualKey, int keyData) + { + virtualKey = GetVirtualKey(virtualKey, keyData); + + s_keyFromVirtualKey.TryGetValue(virtualKey, out var result); + return result; } public static int VirtualKeyFromKey(Key key) { - int result; - s_virtualKeyFromKey.TryGetValue(key, out result); + s_virtualKeyFromKey.TryGetValue(key, out var result); + return result; } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ed32382760..904e122382 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -241,6 +241,170 @@ namespace Avalonia.Win32.Interop MK_XBUTTON2 = 0x0040 } + public enum VirtualKeyStates : int + { + VK_LBUTTON = 0x01, + VK_RBUTTON = 0x02, + VK_CANCEL = 0x03, + VK_MBUTTON = 0x04, + VK_XBUTTON1 = 0x05, + VK_XBUTTON2 = 0x06, + VK_BACK = 0x08, + VK_TAB = 0x09, + VK_CLEAR = 0x0C, + VK_RETURN = 0x0D, + VK_SHIFT = 0x10, + VK_CONTROL = 0x11, + VK_MENU = 0x12, + VK_PAUSE = 0x13, + VK_CAPITAL = 0x14, + VK_KANA = 0x15, + VK_HANGEUL = 0x15, + VK_HANGUL = 0x15, + VK_JUNJA = 0x17, + VK_FINAL = 0x18, + VK_HANJA = 0x19, + VK_KANJI = 0x19, + VK_ESCAPE = 0x1B, + VK_CONVERT = 0x1C, + VK_NONCONVERT = 0x1D, + VK_ACCEPT = 0x1E, + VK_MODECHANGE = 0x1F, + VK_SPACE = 0x20, + VK_PRIOR = 0x21, + VK_NEXT = 0x22, + VK_END = 0x23, + VK_HOME = 0x24, + VK_LEFT = 0x25, + VK_UP = 0x26, + VK_RIGHT = 0x27, + VK_DOWN = 0x28, + VK_SELECT = 0x29, + VK_PRINT = 0x2A, + VK_EXECUTE = 0x2B, + VK_SNAPSHOT = 0x2C, + VK_INSERT = 0x2D, + VK_DELETE = 0x2E, + VK_HELP = 0x2F, + VK_LWIN = 0x5B, + VK_RWIN = 0x5C, + VK_APPS = 0x5D, + VK_SLEEP = 0x5F, + VK_NUMPAD0 = 0x60, + VK_NUMPAD1 = 0x61, + VK_NUMPAD2 = 0x62, + VK_NUMPAD3 = 0x63, + VK_NUMPAD4 = 0x64, + VK_NUMPAD5 = 0x65, + VK_NUMPAD6 = 0x66, + VK_NUMPAD7 = 0x67, + VK_NUMPAD8 = 0x68, + VK_NUMPAD9 = 0x69, + VK_MULTIPLY = 0x6A, + VK_ADD = 0x6B, + VK_SEPARATOR = 0x6C, + VK_SUBTRACT = 0x6D, + VK_DECIMAL = 0x6E, + VK_DIVIDE = 0x6F, + VK_F1 = 0x70, + VK_F2 = 0x71, + VK_F3 = 0x72, + VK_F4 = 0x73, + VK_F5 = 0x74, + VK_F6 = 0x75, + VK_F7 = 0x76, + VK_F8 = 0x77, + VK_F9 = 0x78, + VK_F10 = 0x79, + VK_F11 = 0x7A, + VK_F12 = 0x7B, + VK_F13 = 0x7C, + VK_F14 = 0x7D, + VK_F15 = 0x7E, + VK_F16 = 0x7F, + VK_F17 = 0x80, + VK_F18 = 0x81, + VK_F19 = 0x82, + VK_F20 = 0x83, + VK_F21 = 0x84, + VK_F22 = 0x85, + VK_F23 = 0x86, + VK_F24 = 0x87, + VK_NUMLOCK = 0x90, + VK_SCROLL = 0x91, + VK_OEM_NEC_EQUAL = 0x92, + VK_OEM_FJ_JISHO = 0x92, + VK_OEM_FJ_MASSHOU = 0x93, + VK_OEM_FJ_TOUROKU = 0x94, + VK_OEM_FJ_LOYA = 0x95, + VK_OEM_FJ_ROYA = 0x96, + VK_LSHIFT = 0xA0, + VK_RSHIFT = 0xA1, + VK_LCONTROL = 0xA2, + VK_RCONTROL = 0xA3, + VK_LMENU = 0xA4, + VK_RMENU = 0xA5, + VK_BROWSER_BACK = 0xA6, + VK_BROWSER_FORWARD = 0xA7, + VK_BROWSER_REFRESH = 0xA8, + VK_BROWSER_STOP = 0xA9, + VK_BROWSER_SEARCH = 0xAA, + VK_BROWSER_FAVORITES = 0xAB, + VK_BROWSER_HOME = 0xAC, + VK_VOLUME_MUTE = 0xAD, + VK_VOLUME_DOWN = 0xAE, + VK_VOLUME_UP = 0xAF, + VK_MEDIA_NEXT_TRACK = 0xB0, + VK_MEDIA_PREV_TRACK = 0xB1, + VK_MEDIA_STOP = 0xB2, + VK_MEDIA_PLAY_PAUSE = 0xB3, + VK_LAUNCH_MAIL = 0xB4, + VK_LAUNCH_MEDIA_SELECT = 0xB5, + VK_LAUNCH_APP1 = 0xB6, + VK_LAUNCH_APP2 = 0xB7, + VK_OEM_1 = 0xBA, + VK_OEM_PLUS = 0xBB, + VK_OEM_COMMA = 0xBC, + VK_OEM_MINUS = 0xBD, + VK_OEM_PERIOD = 0xBE, + VK_OEM_2 = 0xBF, + VK_OEM_3 = 0xC0, + VK_OEM_4 = 0xDB, + VK_OEM_5 = 0xDC, + VK_OEM_6 = 0xDD, + VK_OEM_7 = 0xDE, + VK_OEM_8 = 0xDF, + VK_OEM_AX = 0xE1, + VK_OEM_102 = 0xE2, + VK_ICO_HELP = 0xE3, + VK_ICO_00 = 0xE4, + VK_PROCESSKEY = 0xE5, + VK_ICO_CLEAR = 0xE6, + VK_PACKET = 0xE7, + VK_OEM_RESET = 0xE9, + VK_OEM_JUMP = 0xEA, + VK_OEM_PA1 = 0xEB, + VK_OEM_PA2 = 0xEC, + VK_OEM_PA3 = 0xED, + VK_OEM_WSCTRL = 0xEE, + VK_OEM_CUSEL = 0xEF, + VK_OEM_ATTN = 0xF0, + VK_OEM_FINISH = 0xF1, + VK_OEM_COPY = 0xF2, + VK_OEM_AUTO = 0xF3, + VK_OEM_ENLW = 0xF4, + VK_OEM_BACKTAB = 0xF5, + VK_ATTN = 0xF6, + VK_CRSEL = 0xF7, + VK_EXSEL = 0xF8, + VK_EREOF = 0xF9, + VK_PLAY = 0xFA, + VK_ZOOM = 0xFB, + VK_NONAME = 0xFC, + VK_PA1 = 0xFD, + VK_OEM_CLEAR = 0xFE + } + public enum WindowActivate { WA_INACTIVE, @@ -581,6 +745,14 @@ namespace Avalonia.Win32.Interop WM_DISPATCH_WORK_ITEM = WM_USER, } + public enum MapVirtualKeyMapTypes : uint + { + MAPVK_VK_TO_VSC = 0x00, + MAPVK_VSC_TO_VK = 0x01, + MAPVK_VK_TO_CHAR = 0x02, + MAPVK_VSC_TO_VK_EX = 0x03, + } + public enum BitmapCompressionMode : uint { BI_RGB = 0, @@ -756,6 +928,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool GetKeyboardState(byte[] lpKeyState); + [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] + public static extern uint MapVirtualKey(uint uCode, uint uMapType); + [DllImport("user32.dll", EntryPoint = "GetMessageW")] public static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0f5db58dfe..c16b76b539 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -516,7 +516,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_MENUCHAR: @@ -530,7 +530,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_CHAR: // Ignore control chars @@ -795,9 +795,9 @@ namespace Avalonia.Win32 modifiers |= RawInputModifiers.RightMouseButton; if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON1)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON1)) modifiers |= RawInputModifiers.XButton1MouseButton; - if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON2)) + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON2)) modifiers |= RawInputModifiers.XButton2MouseButton; return modifiers; } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs index 4e033be3fb..e523c312ac 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_AddOwner.cs @@ -1,7 +1,6 @@ // 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 Xunit; namespace Avalonia.Base.UnitTests @@ -16,31 +15,12 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AddOwnered_Property_Does_Not_Retain_Validation() - { - var target = new Class2(); - - target.SetValue(Class2.FooProperty, "throw"); - } - private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register( "Foo", - "foodefault", - validate: ValidateFoo); - - private static string ValidateFoo(AvaloniaObject arg1, string arg2) - { - if (arg2 == "throw") - { - throw new IndexOutOfRangeException(); - } - - return arg2; - } + "foodefault"); } private class Class2 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 02600f5e00..44e2976e03 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,14 +16,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [Fact] - public void AddOwnered_Property_Retains_Validation() - { - var target = new Class2(); - - Assert.Throws(() => target.SetValue(Class2.FooProperty, "throw")); - } - [Fact] public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property() { @@ -46,18 +38,7 @@ namespace Avalonia.Base.UnitTests public static readonly AttachedProperty FooProperty = AvaloniaProperty.RegisterAttached( "Foo", - "foodefault", - validate: ValidateFoo); - - private static string ValidateFoo(AvaloniaObject arg1, string arg2) - { - if (arg2 == "throw") - { - throw new IndexOutOfRangeException(); - } - - return arg2; - } + "foodefault"); } private class Class2 : Base diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 23984a7c8d..4c00d2a1ea 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -3,18 +3,15 @@ using System; using System.ComponentModel; -using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; -using Avalonia.Markup.Data; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; -using Avalonia.Diagnostics; using Microsoft.Reactive.Testing; using Moq; using Xunit; @@ -26,13 +23,189 @@ namespace Avalonia.Base.UnitTests [Fact] public void Bind_Sets_Current_Value() { - Class1 target = new Class1(); - Class1 source = new Class1(); + var target = new Class1(); + var source = new Class1(); + var property = Class1.FooProperty; - source.SetValue(Class1.FooProperty, "initial"); - target.Bind(Class1.FooProperty, source.GetObservable(Class1.FooProperty)); + source.SetValue(property, "initial"); + target.Bind(property, source.GetObservable(property)); - Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + Assert.Equal("initial", target.GetValue(property)); + } + + [Fact] + public void Bind_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new Subject>(); + bool raised = false; + + target.PropertyChanged += (s, e) => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "foodefault" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue; + + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + + Assert.True(raised); + } + + [Fact] + public void PropertyChanged_Not_Raised_When_Value_Unchanged() + { + var target = new Class1(); + var source = new Subject>(); + var raised = 0; + + target.PropertyChanged += (s, e) => ++raised; + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + source.OnNext("newvalue"); + + Assert.Equal(1, raised); + } + + [Fact] + public void Setting_LocalValue_Overrides_Binding_Until_Binding_Produces_Next_Value() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.Bind(property, source); + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(property)); + + target.SetValue(property, "bar"); + Assert.Equal("bar", target.GetValue(property)); + + source.OnNext("baz"); + Assert.Equal("baz", target.GetValue(property)); + } + + [Fact] + public void Completing_LocalValue_Binding_Reverts_To_Default_Value_Even_When_Local_Value_Set_Earlier() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.Bind(property, source); + source.OnNext("foo"); + target.SetValue(property, "bar"); + source.OnNext("baz"); + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + } + + [Fact] + public void Completing_LocalValue_Binding_Should_Not_Revert_To_Set_LocalValue() + { + var target = new Class1(); + var source = new BehaviorSubject("bar"); + + target.SetValue(Class1.FooProperty, "foo"); + var sub = target.Bind(Class1.FooProperty, source); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + + sub.Dispose(); + + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Completing_Animation_Binding_Reverts_To_Set_LocalValue() + { + var target = new Class1(); + var source = new Subject(); + var property = Class1.FooProperty; + + target.SetValue(property, "foo"); + target.Bind(property, source, BindingPriority.Animation); + source.OnNext("bar"); + source.OnCompleted(); + + Assert.Equal("foo", target.GetValue(property)); + } + + [Fact] + public void Setting_Style_Value_Overrides_Binding_Permanently() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.Style); + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + + target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + + source.OnNext("baz"); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Second_LocalValue_Binding_Overrides_First() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.LocalValue); + target.Bind(property, source2, BindingPriority.LocalValue); + + source1.OnNext("foo"); + Assert.Equal("foo", target.GetValue(property)); + + source2.OnNext("bar"); + Assert.Equal("bar", target.GetValue(property)); + + source1.OnNext("baz"); + Assert.Equal("bar", target.GetValue(property)); + } + + [Fact] + public void Completing_Second_LocalValue_Binding_Reverts_To_First() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.LocalValue); + target.Bind(property, source2, BindingPriority.LocalValue); + + source1.OnNext("foo"); + source2.OnNext("bar"); + source1.OnNext("baz"); + source2.OnCompleted(); + + Assert.Equal("baz", target.GetValue(property)); + } + + [Fact] + public void Completing_StyleTrigger_Binding_Reverts_To_StyleBinding() + { + var property = Class1.FooProperty; + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); + + target.Bind(property, source1, BindingPriority.Style); + target.Bind(property, source2, BindingPriority.StyleTrigger); + + source1.OnNext("foo"); + source2.OnNext("bar"); + source2.OnCompleted(); + source1.OnNext("baz"); + + Assert.Equal("baz", target.GetValue(property)); } [Fact] @@ -126,7 +299,7 @@ namespace Avalonia.Base.UnitTests public void Observable_Is_Unsubscribed_When_Subscription_Disposed() { var scheduler = new TestScheduler(); - var source = scheduler.CreateColdObservable(); + var source = scheduler.CreateColdObservable(); var target = new Class1(); var subscription = target.Bind(Class1.FooProperty, source); @@ -191,13 +364,13 @@ namespace Avalonia.Base.UnitTests obj2.SetValue(Class1.FooProperty, "second", BindingPriority.Style); - Assert.Equal("second", obj1.GetValue(Class1.FooProperty)); + Assert.Equal("first", obj1.GetValue(Class1.FooProperty)); Assert.Equal("second", obj2.GetValue(Class1.FooProperty)); obj1.SetValue(Class1.FooProperty, "third", BindingPriority.Style); Assert.Equal("third", obj1.GetValue(Class1.FooProperty)); - Assert.Equal("third", obj2.GetValue(Class1.FooProperty)); + Assert.Equal("second", obj2.GetValue(Class1.FooProperty)); } [Fact] @@ -302,41 +475,62 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void BindingError_Does_Not_Cause_Target_Update() + public void Binding_Error_Reverts_To_Default_Value() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); - target.Bind(Class1.QuxProperty, source); - source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error)); + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); - Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } [Fact] - public void BindingNotification_With_FallbackValue_Causes_Target_Update() + public void Binding_Error_With_FallbackValue_Causes_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); - target.Bind(Class1.QuxProperty, source); - source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error, - 8.9)); + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "bar")); - Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void DataValidationError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"))); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void DataValidationError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); } [Fact] public void Bind_Logs_Binding_Error() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var called = false; var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; @@ -354,9 +548,7 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error)); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); Assert.True(called); @@ -367,7 +559,7 @@ namespace Avalonia.Base.UnitTests public async Task Bind_With_Scheduler_Executes_On_Scheduler() { var target = new Class1(); - var source = new Subject(); + var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; var threadingInterfaceMock = new Mock(); @@ -426,13 +618,13 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void IsAnimating_On_Property_With_Animation_Value_Returns_False() + public void IsAnimating_On_Property_With_Animation_Value_Returns_True() { var target = new Class1(); target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); - Assert.False(target.IsAnimating(Class1.FooProperty)); + Assert.True(target.IsAnimating(Class1.FooProperty)); } [Fact] @@ -457,6 +649,30 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void IsAnimating_On_Property_With_Local_Value_And_Animation_Binding_Returns_True() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.SetValue(Class1.FooProperty, "bar"); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_Returns_True_When_Animated_Value_Is_Same_As_Local_Value() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.SetValue(Class1.FooProperty, "foo"); + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + [Fact] public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs new file mode 100644 index 0000000000..3efb926ac3 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -0,0 +1,155 @@ +// 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.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Coercion + { + [Fact] + public void Coerces_Set_Value() + { + var target = new Class1(); + + target.Foo = 150; + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void Coerces_Set_Value_Attached() + { + var target = new Class1(); + + target.SetValue(Class1.AttachedProperty, 150); + + Assert.Equal(100, target.GetValue(Class1.AttachedProperty)); + } + + [Fact] + public void Coerces_Bound_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void CoerceValue_Updates_Value() + { + var target = new Class1 { Foo = 99 }; + + Assert.Equal(99, target.Foo); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() + { + var target = new Class1(); + + target.Foo = 150; + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding() + { + var target = new Class1(); + var source1 = new Subject>(); + var source2 = new Subject>(); + + target.Bind(Class1.FooProperty, source1); + source1.OnNext(150); + + target.Bind(Class1.FooProperty, source2); + source2.OnNext(160); + + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + source2.OnCompleted(); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coercion_Can_Be_Overridden() + { + var target = new Class2(); + + target.Foo = 150; + + Assert.Equal(-150, target.Foo); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + coerce: CoerceFoo); + + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return Math.Min(((Class1)instance).MaxFoo, value); + } + } + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + Class1.FooProperty.AddOwner(); + + static Class2() + { + FooProperty.OverrideMetadata( + new StyledPropertyMetadata( + coerce: CoerceFoo)); + } + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return -value; + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index 428f878945..e8cc71c723 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -11,59 +11,31 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_DataValidation { [Fact] - public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation() { var target = new Class1(); + var source = new Subject>(); - target.SetValue(Class1.NonValidatedDirectProperty, 6); + target.Bind(Class1.NonValidatedProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); Assert.Empty(target.Notifications); } [Fact] - public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() + public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() { var target = new Class1(); + var source = new Subject>(); - target.SetValue(Class1.NonValidatedDirectProperty, 6); - - Assert.Empty(target.Notifications); - } - - [Fact] - public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation() - { - var target = new Class1(); - - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7)); - - Assert.Equal( - new[] - { - new BindingNotification(6), - new BindingNotification(new Exception(), BindingErrorType.Error), - new BindingNotification(new Exception(), BindingErrorType.DataValidationError), - new BindingNotification(7), - }, - target.Notifications.AsEnumerable()); - } - - [Fact] - public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() - { - var source = new Subject(); - var target = new Class1 - { - [!Class1.NonValidatedProperty] = source.ToBinding(), - }; - - source.OnNext(new BindingNotification(6)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - source.OnNext(new BindingNotification(7)); + target.Bind(Class1.NonValidatedDirectProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); Assert.Empty(target.Notifications); } @@ -71,26 +43,23 @@ namespace Avalonia.Base.UnitTests [Fact] public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() { - var source = new Subject(); - var target = new Class1 - { - [!Class1.ValidatedDirectIntProperty] = source.ToBinding(), - }; - - source.OnNext(new BindingNotification(6)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); - source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - source.OnNext(new BindingNotification(7)); - - Assert.Equal( - new[] - { - new BindingNotification(6), - new BindingNotification(new Exception(), BindingErrorType.Error), - new BindingNotification(new Exception(), BindingErrorType.DataValidationError), - new BindingNotification(7), - }, - target.Notifications.AsEnumerable()); + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.ValidatedDirectIntProperty, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(7); + + var result = target.Notifications.Cast>().ToList(); + Assert.Equal(4, result.Count); + Assert.Equal(BindingValueType.Value, result[0].Type); + Assert.Equal(6, result[0].Value); + Assert.Equal(BindingValueType.BindingError, result[1].Type); + Assert.Equal(BindingValueType.DataValidationError, result[2].Type); + Assert.Equal(BindingValueType.Value, result[3].Type); + Assert.Equal(7, result[3].Value); } [Fact] @@ -171,11 +140,13 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } - public IList Notifications { get; } = new List(); + public IList Notifications { get; } = new List(); - protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValue value) { - Notifications.Add(notification); + Notifications.Add(value); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 980cbfaaf8..4110c3771f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -7,12 +7,9 @@ using System.ComponentModel; using System.Reactive.Subjects; using System.Threading; using System.Threading.Tasks; -using Avalonia; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Moq; using Xunit; @@ -22,7 +19,7 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Direct { [Fact] - public void GetValue_Gets_Value() + public void GetValue_Gets_Default_Value() { var target = new Class1(); @@ -109,6 +106,62 @@ namespace Avalonia.Base.UnitTests Assert.True(raised); } + [Fact] + public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("Kups", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Setting_Object_Property_To_DoNothing_Does_Nothing() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing); + + Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Bind_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new Subject>(); + bool raised = false; + + target.PropertyChanged += (s, e) => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "initial" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue; + + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + + Assert.True(raised); + } + + [Fact] + public void PropertyChanged_Not_Raised_When_Value_Unchanged() + { + var target = new Class1(); + var source = new Subject>(); + var raised = 0; + + target.PropertyChanged += (s, e) => ++raised; + target.Bind(Class1.FooProperty, source); + source.OnNext("newvalue"); + source.OnNext("newvalue"); + + Assert.Equal(1, raised); + } + [Fact] public void SetValue_On_Unregistered_Property_Throws_Exception() { @@ -117,6 +170,35 @@ namespace Avalonia.Base.UnitTests Assert.Throws(() => target.SetValue(Class1.BarProperty, "value")); } + [Fact] + public void ClearValue_Restores_Default_value() + { + var target = new Class1(); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Raises_PropertyChanged() + { + Class1 target = new Class1(); + var raised = 0; + + target.SetValue(Class1.FooProperty, "newvalue"); + target.PropertyChanged += (s, e) => + { + Assert.Same(target, s); + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal("newvalue", (string)e.OldValue); + Assert.Equal("unset", (string)e.NewValue); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(1, raised); + } + [Fact] public void GetObservable_Returns_Values() { @@ -170,7 +252,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Bind_NonGeneric_Uses_UnsetValue() + public void Bind_NonGeneric_Accepts_UnsetValue() { var target = new Class1(); var source = new Subject(); @@ -194,7 +276,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(45); - Assert.Null(target.Foo); + Assert.Equal("unset", target.Foo); } [Fact] @@ -207,7 +289,7 @@ namespace Avalonia.Base.UnitTests source.OnNext("foo"); - Assert.Equal(0, target.Baz); + Assert.Equal(-1, target.Baz); } [Fact] @@ -358,31 +440,67 @@ namespace Avalonia.Base.UnitTests Assert.True(raised); } + [Fact] + public void Binding_Error_Reverts_To_Default_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal("unset", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Binding_Error_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void DataValidationError_Does_Not_Cause_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.DataValidationError)); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"))); Assert.Equal("initial", target.GetValue(Class1.FooProperty)); } + [Fact] + public void DataValidationError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(BindingValue.DataValidationError(new InvalidOperationException("Foo"), "bar")); + + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void BindingError_With_FallbackValue_Causes_Target_Update() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingNotification( - new InvalidOperationException("Foo"), - BindingErrorType.Error, - "fallback")); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Foo"), "fallback")); Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); } @@ -391,7 +509,7 @@ namespace Avalonia.Base.UnitTests public void Binding_To_Direct_Property_Logs_BindingError() { var target = new Class1(); - var source = new Subject(); + var source = new Subject>(); var called = false; LogCallback checkLogMessage = (level, area, src, mt, pv) => @@ -412,7 +530,7 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.FooProperty, source); source.OnNext("baz"); - source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error)); + source.OnNext(BindingValue.BindingError(new InvalidOperationException("Binding Error Message"))); } Assert.True(called); @@ -447,7 +565,8 @@ namespace Avalonia.Base.UnitTests "foo", o => "foo", null, - new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay), + false); var bar = foo.AddOwner(o => "bar"); Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); @@ -461,7 +580,8 @@ namespace Avalonia.Base.UnitTests "foo", o => "foo", null, - new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); + new DirectPropertyMetadata(defaultBindingMode: BindingMode.TwoWay), + false); var bar = foo.AddOwner(o => "bar", defaultBindingMode: BindingMode.OneWayToSource); Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); @@ -527,10 +647,18 @@ namespace Avalonia.Base.UnitTests o => o.DoubleValue, (o, v) => o.DoubleValue = v); + public static readonly DirectProperty FrankProperty = + AvaloniaProperty.RegisterDirect( + nameof(Frank), + o => o.Frank, + (o, v) => o.Frank = v, + unsetValue: "Kups"); + private string _foo = "initial"; private readonly string _bar = "bar"; private int _baz = 5; private double _doubleValue; + private object _frank; public string Foo { @@ -554,6 +682,12 @@ namespace Avalonia.Base.UnitTests get { return _doubleValue; } set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); } } + + public object Frank + { + get { return _frank; } + set { SetAndRaise(FrankProperty, ref _frank, value); } + } } private class Class2 : AvaloniaObject @@ -609,4 +743,4 @@ namespace Avalonia.Base.UnitTests } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs index 740023fd37..b496b30ce3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetValue.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 System.Reactive.Subjects; using Xunit; namespace Avalonia.Base.UnitTests @@ -27,11 +28,23 @@ namespace Avalonia.Base.UnitTests [Fact] public void GetValue_Returns_Set_Value() { - Class1 target = new Class1(); + var target = new Class1(); + var property = Class1.FooProperty; + + target.SetValue(property, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(property)); + } + + [Fact] + public void GetValue_Returns_Bound_Value() + { + var target = new Class1(); + var property = Class1.FooProperty; - target.SetValue(Class1.FooProperty, "newvalue"); + target.Bind(property, new BehaviorSubject("newvalue")); - Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal("newvalue", target.GetValue(property)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index a56cd717b9..40631d04cf 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -20,6 +20,27 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void ClearValue_Raises_PropertyChanged() + { + Class1 target = new Class1(); + var raised = 0; + + target.SetValue(Class1.FooProperty, "newvalue"); + target.PropertyChanged += (s, e) => + { + Assert.Same(target, s); + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal("newvalue", (string)e.OldValue); + Assert.Equal("foodefault", (string)e.NewValue); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(1, raised); + } + [Fact] public void SetValue_Sets_Value() { @@ -59,6 +80,25 @@ namespace Avalonia.Base.UnitTests Assert.True(raised); } + [Fact] + public void SetValue_Style_Priority_Raises_PropertyChanged() + { + Class1 target = new Class1(); + bool raised = false; + + target.PropertyChanged += (s, e) => + { + raised = s == target && + e.Property == Class1.FooProperty && + (string)e.OldValue == "foodefault" && + (string)e.NewValue == "newvalue"; + }; + + target.SetValue(Class1.FooProperty, "newvalue", BindingPriority.Style); + + Assert.True(raised); + } + [Fact] public void SetValue_Doesnt_Raise_PropertyChanged_If_Value_Not_Changed() { @@ -177,6 +217,28 @@ namespace Avalonia.Base.UnitTests Assert.Equal("three", target.GetValue(Class1.FooProperty)); } + [Fact] + public void SetValue_Style_Doesnt_Override_LocalValue() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "two", BindingPriority.Style); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void SetValue_LocalValue_Overrides_Style() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "one", BindingPriority.Style); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "two", BindingPriority.LocalValue); + Assert.Equal("two", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Setting_UnsetValue_Reverts_To_Default_Value() { @@ -188,10 +250,35 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void Setting_Object_Property_To_UnsetValue_Reverts_To_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("Kups", target.GetValue(Class1.FrankProperty)); + } + + [Fact] + public void Setting_Object_Property_To_DoNothing_Does_Nothing() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FrankProperty, "newvalue"); + target.SetValue(Class1.FrankProperty, BindingOperations.DoNothing); + + Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register("Foo", "foodefault"); + + public static readonly StyledProperty FrankProperty = + AvaloniaProperty.Register("Frank", "Kups"); } private class Class2 : Class1 diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index f0e93dbb3a..391b379c51 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Subjects; +using Avalonia.Controls; using Xunit; namespace Avalonia.Base.UnitTests @@ -10,147 +11,87 @@ namespace Avalonia.Base.UnitTests public class AvaloniaObjectTests_Validation { [Fact] - public void SetValue_Causes_Validation() + public void Registration_Throws_If_DefaultValue_Fails_Validation() { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 25)); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => + new StyledProperty( + "BadDefault", + typeof(Class1), + new StyledPropertyMetadata(101), + validate: Class1.ValidateFoo)); } [Fact] - public void SetValue_Causes_Coercion() + public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation() { - var target = new Class1(); - - target.SetValue(Class1.QuxProperty, 5); - Assert.Equal(5, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, -5); - Assert.Equal(0, target.GetValue(Class1.QuxProperty)); - target.SetValue(Class1.QuxProperty, 15); - Assert.Equal(10, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => Class1.FooProperty.OverrideDefaultValue(101)); } [Fact] - public void Revalidate_Causes_Recoercion() + public void SetValue_Throws_If_Fails_Validation() { var target = new Class1(); - target.SetValue(Class1.QuxProperty, 7); - Assert.Equal(7, target.GetValue(Class1.QuxProperty)); - target.MaxQux = 5; - target.Revalidate(Class1.QuxProperty); + Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); } [Fact] - public void Validation_Can_Be_Overridden() + public void SetValue_Throws_If_Fails_Validation_Attached() { - var target = new Class2(); - Assert.Throws(() => target.SetValue(Class1.QuxProperty, 5)); - } + var target = new Class1(); - [Fact] - public void Validation_Can_Be_Overridden_With_Null() - { - var target = new Class3(); - target.SetValue(Class1.QuxProperty, 50); - Assert.Equal(50, target.GetValue(Class1.QuxProperty)); + Assert.Throws(() => target.SetValue(Class1.AttachedProperty, 101)); } [Fact] - public void Binding_To_UnsetValue_Doesnt_Throw() + public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() { var target = new Class1(); - var source = new Subject(); + var source = new Subject(); - target.Bind(Class1.QuxProperty, source); + target.Bind(Class1.FooProperty, source); + source.OnNext(150); - source.OnNext(AvaloniaProperty.UnsetValue); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } [Fact] - public void Attached_Property_Should_Be_Validated() + public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { - var target = new Class2(); - - target.SetValue(Class1.AttachedProperty, 15); - Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); - } + var target = new Class1(); + var source1 = new Subject(); + var source2 = new Subject(); - [Fact] - public void PropertyChanged_Event_Uses_Coerced_Value() - { - var inst = new Class1(); - inst.PropertyChanged += (sender, e) => - { - Assert.Equal(10, e.NewValue); - }; + target.Bind(Class1.FooProperty, source1); + target.Bind(Class1.FooProperty, source2); + source1.OnNext(42); + source2.OnNext(150); - inst.SetValue(Class1.QuxProperty, 15); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } private class Class1 : AvaloniaObject { - public static readonly StyledProperty QuxProperty = - AvaloniaProperty.Register("Qux", validate: Validate); + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + validate: ValidateFoo); public static readonly AttachedProperty AttachedProperty = - AvaloniaProperty.RegisterAttached("Attached", validate: Validate); + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + validate: ValidateFoo); - public Class1() + public static bool ValidateFoo(int value) { - MaxQux = 10; - ErrorQux = 20; - } - - public int MaxQux { get; set; } - - public int ErrorQux { get; } - - private static int Validate(Class1 instance, int value) - { - if (value > instance.ErrorQux) - { - throw new ArgumentOutOfRangeException(); - } - - return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux); - } - - private static int Validate(Class2 instance, int value) - { - return Math.Min(value, 10); + return value < 100; } } private class Class2 : AvaloniaObject { - public static readonly StyledProperty QuxProperty = - Class1.QuxProperty.AddOwner(); - - static Class2() - { - QuxProperty.OverrideValidation(Validate); - } - - private static int Validate(Class2 instance, int value) - { - if (value < 100) - { - throw new ArgumentOutOfRangeException(); - } - - return value; - } - } - - private class Class3 : Class2 - { - static Class3() - { - QuxProperty.OverrideValidation(null); - } } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 2933893f7a..90b8bcff63 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -140,6 +140,42 @@ namespace Avalonia.Base.UnitTests { OverrideMetadata(typeof(T), metadata); } + + internal override void NotifyInitialized(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override IDisposable RouteBind( + IAvaloniaObject o, + IObservable> source, + BindingPriority priority) + { + throw new NotImplementedException(); + } + + internal override void RouteClearValue(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override object RouteGetValue(IAvaloniaObject o) + { + throw new NotImplementedException(); + } + + internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) + { + throw new NotImplementedException(); + } + + internal override void RouteSetValue( + IAvaloniaObject o, + object value, + BindingPriority priority) + { + throw new NotImplementedException(); + } } private class Class1 : AvaloniaObject diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index c472fffb38..4eabc70cca 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -16,39 +16,39 @@ namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_DataValidation : IClassFixture { - [Fact] - public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() - { - var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); - var validationMessageFound = false; - - observer.OfType() - .Where(x => x.ErrorType == BindingErrorType.DataValidationError) - .Subscribe(_ => validationMessageFound = true); - observer.SetValue(-5); - - Assert.False(validationMessageFound); - - GC.KeepAlive(data); - } - - [Fact] - public void Exception_Validation_Sends_DataValidationError() - { - var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); - var validationMessageFound = false; - - observer.OfType() - .Where(x => x.ErrorType == BindingErrorType.DataValidationError) - .Subscribe(_ => validationMessageFound = true); - observer.SetValue(-5); - - Assert.True(validationMessageFound); - - GC.KeepAlive(data); - } + ////[Fact] + ////public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() + ////{ + //// var data = new ExceptionTest { MustBePositive = 5 }; + //// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); + //// var validationMessageFound = false; + + //// observer.OfType() + //// .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + //// .Subscribe(_ => validationMessageFound = true); + //// observer.SetValue(-5); + + //// Assert.False(validationMessageFound); + + //// GC.KeepAlive(data); + ////} + + ////[Fact] + ////public void Exception_Validation_Sends_DataValidationError() + ////{ + //// var data = new ExceptionTest { MustBePositive = 5 }; + //// var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); + //// var validationMessageFound = false; + + //// observer.OfType() + //// .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + //// .Subscribe(_ => validationMessageFound = true); + //// observer.SetValue(-5); + + //// Assert.True(validationMessageFound); + + //// GC.KeepAlive(data); + ////} [Fact] public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index fe7186e417..c2a8b03f15 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -34,8 +34,9 @@ namespace Avalonia.Base.UnitTests var target = new DirectProperty( "test", o => null, - null, - new DirectPropertyMetadata()); + null, + new DirectPropertyMetadata(), + false); Assert.True(target.IsDirect); } @@ -71,17 +72,6 @@ namespace Avalonia.Base.UnitTests Assert.Same(p1.Initialized, p2.Initialized); } - [Fact] - public void IsAnimating_On_DirectProperty_With_Binding_Returns_False() - { - var target = new Class1(); - var source = new BehaviorSubject("foo"); - - target.Bind(Class1.FooProperty, source, BindingPriority.Animation); - - Assert.False(target.IsAnimating(Class1.FooProperty)); - } - private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 63e1790cce..8c76445645 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -1,314 +1,239 @@ -// 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.Utilities; -using Moq; -using System; +using System; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using System.Reactive.Disposables; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests { public class PriorityValueTests { - private static readonly AvaloniaProperty TestProperty = - new StyledProperty( - "Test", - typeof(PriorityValueTests), - new StyledPropertyMetadata()); + private static readonly IValueSink NullSink = Mock.Of(); + private static readonly IAvaloniaObject Owner = Mock.Of(); + private static readonly StyledProperty TestProperty = new StyledProperty( + "Test", + typeof(PriorityValueTests), + new StyledPropertyMetadata()); [Fact] - public void Initial_Value_Should_Be_UnsetValue() + public void Constructor_Should_Set_Value_Based_On_Initial_Entry() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink, + new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); - Assert.Same(AvaloniaProperty.UnsetValue, target.Value); + Assert.Equal("1", target.Value.Value); + Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); } [Fact] - public void First_Binding_Sets_Value() + public void SetValue_LocalValue_Should_Not_Add_Entries() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); - target.Add(Single("foo"), 0); + target.SetValue("1", BindingPriority.LocalValue); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("foo", target.Value); + Assert.Empty(target.Entries); } [Fact] - public void Changing_Binding_Should_Set_Value() + public void SetValue_Non_LocalValue_Should_Add_Entries() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("foo"); + var target = new PriorityValue( + Owner, + TestProperty, + NullSink); - target.Add(subject, 0); - Assert.Equal("foo", target.Value); - subject.OnNext("bar"); - Assert.Equal("bar", target.Value); - } + target.SetValue("1", BindingPriority.Style); + target.SetValue("2", BindingPriority.Animation); - [Fact] - public void Setting_Direct_Value_Should_Override_Binding() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var result = target.Entries + .OfType>() + .Select(x => x.Value.Value) + .ToList(); - target.Add(Single("foo"), 0); - target.SetValue("bar", 0); - - Assert.Equal("bar", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void Binding_Firing_Should_Override_Direct_Value() + public void Binding_With_Same_Priority_Should_Be_Appended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("initial"); - - target.Add(source, 0); - Assert.Equal("initial", target.Value); - target.SetValue("first", 0); - Assert.Equal("first", target.Value); - source.OnNext("second"); - Assert.Equal("second", target.Value); - } + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - [Fact] - public void Earlier_Binding_Firing_Should_Not_Override_Later() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var nonActive = new BehaviorSubject("na"); - var source = new BehaviorSubject("initial"); - - target.Add(nonActive, 1); - target.Add(source, 1); - Assert.Equal("initial", target.Value); - target.SetValue("first", 1); - Assert.Equal("first", target.Value); - nonActive.OnNext("second"); - Assert.Equal("first", target.Value); - } + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.LocalValue); - [Fact] - public void Binding_Completing_Should_Revert_To_Direct_Value() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("initial"); - - target.Add(source, 0); - Assert.Equal("initial", target.Value); - target.SetValue("first", 0); - Assert.Equal("first", target.Value); - source.OnNext("second"); - Assert.Equal("second", target.Value); - source.OnCompleted(); - Assert.Equal("first", target.Value); - } - - [Fact] - public void Binding_With_Lower_Priority_Has_Precedence() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 1); - target.Add(Single("bar"), 0); - target.Add(Single("baz"), 1); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - Assert.Equal("bar", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void Later_Binding_With_Same_Priority_Should_Take_Precedence() + public void Binding_With_Higher_Priority_Should_Be_Appended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - target.Add(Single("foo"), 1); - target.Add(Single("bar"), 0); - target.Add(Single("baz"), 0); - target.Add(Single("qux"), 1); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Animation); - Assert.Equal("baz", target.Value); - } + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - [Fact] - public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); - - target.Add(Single("foo"), 0); - target.Add(subject, 1); - Assert.Equal("foo", target.Value); - subject.OnNext("baz"); - Assert.Equal("foo", target.Value); + Assert.Equal(new[] { "1", "2" }, result); } [Fact] - public void UnsetValue_Should_Fall_Back_To_Next_Binding() + public void Binding_With_Lower_Priority_Should_Be_Prepended() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); - target.Add(subject, 0); - target.Add(Single("foo"), 1); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Style); - Assert.Equal("bar", target.Value); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - subject.OnNext(AvaloniaProperty.UnsetValue); - - Assert.Equal("foo", target.Value); + Assert.Equal(new[] { "2", "1" }, result); } [Fact] - public void Adding_Value_Should_Call_OnNext() + public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() { - var owner = GetMockOwner(); - var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 0); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo")); - } - - [Fact] - public void Changing_Value_Should_Call_OnNext() - { - var owner = GetMockOwner(); - var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("foo"); + target.AddBinding(source1, BindingPriority.LocalValue); + target.AddBinding(source2, BindingPriority.Style); + target.AddBinding(source3, BindingPriority.Style); - target.Add(subject, 0); - subject.OnNext("bar"); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar")); + Assert.Equal(new[] { "2", "3", "1" }, result); } [Fact] - public void Disposing_A_Binding_Should_Revert_To_Next_Value() + public void Competed_Binding_Should_Be_Removed() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - - target.Add(Single("foo"), 0); - var disposable = target.Add(Single("bar"), 0); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - Assert.Equal("bar", target.Value); - disposable.Dispose(); - Assert.Equal("foo", target.Value); - } - - [Fact] - public void Disposing_A_Binding_Should_Remove_BindingEntry() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Style).Start(); + source3.OnCompleted(); - target.Add(Single("foo"), 0); - var disposable = target.Add(Single("bar"), 0); + var result = target.Entries + .OfType>() + .Select(x => x.Source) + .OfType() + .Select(x => x.Id) + .ToList(); - Assert.Equal(2, target.GetBindings().Count()); - disposable.Dispose(); - Assert.Single(target.GetBindings()); + Assert.Equal(new[] { "2", "1" }, result); } [Fact] - public void Completing_A_Binding_Should_Revert_To_Previous_Binding() + public void Value_Should_Come_From_Last_Entry() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); + var source2 = new Source("2"); + var source3 = new Source("3"); - target.Add(Single("foo"), 0); - target.Add(source, 0); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.AddBinding(source2, BindingPriority.Style).Start(); + target.AddBinding(source3, BindingPriority.Style).Start(); - Assert.Equal("bar", target.Value); - source.OnCompleted(); - Assert.Equal("foo", target.Value); + Assert.Equal("1", target.Value.Value); } [Fact] - public void Completing_A_Binding_Should_Revert_To_Lower_Priority() + public void LocalValue_Should_Override_LocalValue_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var source = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.Add(Single("foo"), 1); - target.Add(source, 0); + target.AddBinding(source1, BindingPriority.LocalValue).Start(); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal("bar", target.Value); - source.OnCompleted(); - Assert.Equal("foo", target.Value); + Assert.Equal("2", target.Value.Value); } [Fact] - public void Completing_A_Binding_Should_Remove_BindingEntry() + public void LocalValue_Should_Override_Style_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); - var subject = new BehaviorSubject("bar"); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.Add(Single("foo"), 0); - target.Add(subject, 0); + target.AddBinding(source1, BindingPriority.Style).Start(); + target.SetValue("2", BindingPriority.LocalValue); - Assert.Equal(2, target.GetBindings().Count()); - subject.OnCompleted(); - Assert.Single(target.GetBindings()); + Assert.Equal("2", target.Value.Value); } [Fact] - public void Direct_Value_Should_Be_Coerced() + public void LocalValue_Should_Not_Override_Animation_Binding() { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); + var target = new PriorityValue(Owner, TestProperty, NullSink); + var source1 = new Source("1"); - target.SetValue(5, 0); - Assert.Equal(5, target.Value); - target.SetValue(15, 0); - Assert.Equal(10, target.Value); - } + target.AddBinding(source1, BindingPriority.Animation).Start(); + target.SetValue("2", BindingPriority.LocalValue); - [Fact] - public void Bound_Value_Should_Be_Coerced() - { - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); - var source = new Subject(); - - target.Add(source, 0); - source.OnNext(5); - Assert.Equal(5, target.Value); - source.OnNext(15); - Assert.Equal(10, target.Value); + Assert.Equal("1", target.Value.Value); } - [Fact] - public void Revalidate_Should_ReCoerce_Value() + private class Source : IObservable> { - var max = 10; - var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max)); - var source = new Subject(); - - target.Add(source, 0); - source.OnNext(5); - Assert.Equal(5, target.Value); - source.OnNext(15); - Assert.Equal(10, target.Value); - max = 12; - target.Revalidate(); - Assert.Equal(12, target.Value); - } + private IObserver> _observer; - /// - /// Returns an observable that returns a single value but does not complete. - /// - /// The type of the observable. - /// The value. - /// The observable. - private IObservable Single(T value) - { - return Observable.Never().StartWith(value); - } + public Source(string id) => Id = id; + public string Id { get; } - private static Mock GetMockOwner() - { - var owner = new Mock(); - owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter()); - return owner; + public IDisposable Subscribe(IObserver> observer) + { + _observer = observer; + observer.OnNext(Id); + return Disposable.Empty; + } + + public void OnCompleted() => _observer.OnCompleted(); } } } diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 45fc68ac96..e3650d9f4b 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -35,7 +35,7 @@ namespace Avalonia.Benchmarks.Base class Class1 : AvaloniaObject { - public static readonly AvaloniaProperty IntProperty = + public static readonly StyledProperty IntProperty = AvaloniaProperty.Register("Int"); } } diff --git a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs new file mode 100644 index 0000000000..b70ae19275 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; +using System.Text; +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class StyledPropertyBenchmarks + { + [Benchmark] + public void Set_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void Set_Int_Property_Multiple_Priorities() + { + var obj = new StyledClass(); + var value = 0; + + for (var i = 0; i < 100; ++i) + { + for (var p = BindingPriority.Animation; p <= BindingPriority.Style; ++p) + { + obj.SetValue(StyledClass.IntValueProperty, value++, p); + } + } + } + + [Benchmark] + public void Set_Int_Property_TemplatedParent() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.SetValue(StyledClass.IntValueProperty, obj.IntValue + 1, BindingPriority.TemplatedParent); + } + } + + [Benchmark] + public void Bind_Int_Property_LocalValue() + { + var obj = new StyledClass(); + var source = new Subject>(); + + obj.Bind(StyledClass.IntValueProperty, source); + + for (var i = 0; i < 100; ++i) + { + source.OnNext(i); + } + } + + [Benchmark] + public void Bind_Int_Property_Multiple_Priorities() + { + var obj = new StyledClass(); + var sources = new List>>(); + var value = 0; + + for (var p = BindingPriority.Animation; p <= BindingPriority.Style; ++p) + { + var source = new Subject>(); + sources.Add(source); + obj.Bind(StyledClass.IntValueProperty, source, p); + } + + for (var i = 0; i < 100; ++i) + { + foreach (var source in sources) + { + source.OnNext(value++); + } + } + } + + [Benchmark] + public void Set_Validated_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.ValidatedIntValue += 1; + } + } + + [Benchmark] + public void Set_Coerced_Int_Property_LocalValue() + { + var obj = new StyledClass(); + + for (var i = 0; i < 100; ++i) + { + obj.CoercedIntValue += 1; + } + } + + class StyledClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + public static readonly StyledProperty ValidatedIntValueProperty = + AvaloniaProperty.Register(nameof(ValidatedIntValue), validate: ValidateIntValue); + public static readonly StyledProperty CoercedIntValueProperty = + AvaloniaProperty.Register(nameof(CoercedIntValue), coerce: CoerceIntValue); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int ValidatedIntValue + { + get => GetValue(ValidatedIntValueProperty); + set => SetValue(ValidatedIntValueProperty, value); + } + + public int CoercedIntValue + { + get => GetValue(CoercedIntValueProperty); + set => SetValue(CoercedIntValueProperty, value); + } + + private static bool ValidateIntValue(int arg) + { + return arg < 1000; + } + + private static int CoerceIntValue(IAvaloniaObject arg1, int arg2) + { + return Math.Min(1000, arg2); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs index 7167eafc87..9a756aaf0b 100644 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -21,6 +21,8 @@ namespace Avalonia.Benchmarks public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + public IVisual HitTestFirst(Point p, IVisual root, Func filter) => null; + public void Paint(Rect rect) { } diff --git a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs new file mode 100644 index 0000000000..0ac96c1103 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class SelectorBenchmark + { + private readonly Control _notMatchingControl; + private readonly Calendar _matchingControl; + private readonly Selector _isCalendarSelector; + private readonly Selector _classSelector; + + public SelectorBenchmark() + { + _notMatchingControl = new Control(); + _matchingControl = new Calendar(); + + const string className = "selector-class"; + + _matchingControl.Classes.Add(className); + + _isCalendarSelector = Selectors.Is(null); + _classSelector = Selectors.Class(null, className); + } + + [Benchmark] + public SelectorMatch IsSelector_NoMatch() + { + return _isCalendarSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch IsSelector_Match() + { + return _isCalendarSelector.Match(_matchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_NoMatch() + { + return _classSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_Match() + { + return _classSelector.Match(_matchingControl); + } + } +} diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj new file mode 100644 index 0000000000..7fec6f2770 --- /dev/null +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0;net47 + latest + Library + true + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs new file mode 100644 index 0000000000..a1a734f650 --- /dev/null +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using Avalonia.Collections; +using Xunit; + +namespace Avalonia.Controls.DataGrid.UnitTests.Collections +{ + + public class DataGridSortDescriptionTests + { + [Fact] + public void OrderBy_Orders_Correctly_When_Ascending() + { + var items = new[] + { + new Item("b", "b"), + new Item("a", "a"), + new Item("c", "c"), + }; + var expectedResult = items.OrderBy(i => i.Prop1).ToList(); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: false); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.OrderBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void OrderBy_Orders_Correctly_When_Descending() + { + var items = new[] + { + new Item("b", "b"), + new Item("a", "a"), + new Item("c", "c"), + }; + var expectedResult = items.OrderByDescending(i => i.Prop1).ToList(); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: true); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.OrderBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void ThenBy_Orders_Correctly_When_Ascending() + { + // Casting nonsense below because IOrderedEnumerable isn't covariant in full framework and we need an + // object of type IOrderedEnumerable for DataGridSortDescription.ThenBy + var items = new[] + { + (object)new Item("a", "b"), + new Item("a", "a"), + new Item("a", "c"), + }.OrderBy(i => ((Item)i).Prop1); + var expectedResult = new[] + { + new Item("a", "a"), + new Item("a", "b"), + new Item("a", "c"), + }; + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: false); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.ThenBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void ThenBy_Orders_Correctly_When_Descending() + { + // Casting nonsense below because IOrderedEnumerable isn't covariant in full framework and we need an + // object of type IOrderedEnumerable for DataGridSortDescription.ThenBy + var items = new[] + { + (object)new Item("a", "b"), + new Item("a", "a"), + new Item("a", "c"), + }.OrderBy(i => ((Item)i).Prop1); + var expectedResult = new[] + { + new Item("a", "c"), + new Item("a", "b"), + new Item("a", "a"), + }; + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: true); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.ThenBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + private class Item : IEquatable + { + public Item(string prop1, string prop2) + { + Prop1 = prop1; + Prop2 = prop2; + } + + public string Prop1 { get; } + public string Prop2 { get; } + + public bool Equals(Item other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Prop1 == other.Prop1 && Prop2 == other.Prop2; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Item) obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((Prop1 != null ? Prop1.GetHashCode() : 0) * 397) ^ (Prop2 != null ? Prop2.GetHashCode() : 0); + } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index 71d0d1e328..d3c0e29eca 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -62,10 +62,59 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + [Fact] + public void Measure_Should_Return_Correct_Size_With_StretchDirection_DownOnly() + { + var bitmap = CreateBitmap(50, 100); + var target = new Image(); + target.StretchDirection = StretchDirection.DownOnly; + target.Source = bitmap; + + target.Measure(new Size(150, 150)); + + Assert.Equal(new Size(50, 100), target.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(200, double.PositiveInfinity)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, 400)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(new Size(50, 100), image.DesiredSize); + } + [Fact] public void Arrange_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -79,7 +128,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -93,7 +142,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -107,7 +156,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -117,5 +166,10 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(25, 100), target.Bounds.Size); } + + private IBitmap CreateBitmap(int width, int height) + { + return Mock.Of(x => x.Size == new Size(width, height)); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index f75f6fcf91..7db96a8db7 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -194,7 +194,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private class TemplatedControlWithPopup : TemplatedControl { - public static readonly AvaloniaProperty PopupContentProperty = + public static readonly StyledProperty PopupContentProperty = AvaloniaProperty.Register(nameof(PopupContent)); public TemplatedControlWithPopup() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d819581000..696c0dbf46 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void SelectedIndex_Should_Be_Minus_1_Without_Initialize() + { + var items = new[] + { + new Item(), + new Item(), + }; + + var target = new ListBox(); + target.Items = items; + target.Template = Template(); + target.DataContext = new object(); + + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs index 4f4ab47b0a..9acd42aba6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -1,5 +1,4 @@ using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests Assert.Null(threeStateButton.IsChecked); } + [Fact] + public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes() + { + var threeStateButton = new ToggleButton(); + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.IsChecked = true; + Assert.True(checkedRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.IsChecked = false; + Assert.True(uncheckedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.IsChecked = null; + Assert.True(indeterminateRaised); + } + + [Fact] + public void ToggleButton_Events_Are_Raised_When_Toggling() + { + var threeStateButton = new TestToggleButton { IsThreeState = true }; + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.Toggle(); + Assert.True(checkedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.Toggle(); + Assert.True(indeterminateRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.Toggle(); + Assert.True(uncheckedRaised); + } + private class Class1 : NotifyingBase { private bool _foo; @@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests set { nullableFoo = value; RaisePropertyChanged(); } } } + + private class TestToggleButton : ToggleButton + { + public new void Toggle() => base.Toggle(); + } } } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 82471915f4..3320bcebca 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -36,6 +36,30 @@ namespace Avalonia.Direct2D1.UnitTests.Media } } + [Fact] + public void Should_Create_Typeface_From_Fallback_Bold() + { + 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"), FontWeight.Bold)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + [Fact] public void Should_Create_Typeface_For_Unknown_Font() { diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 214aead521..68d4e3ada9 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -238,6 +238,9 @@ namespace Avalonia.Input.UnitTests { renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(new[] { hit }); + + renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit); } private IDisposable TestApplication(IRenderer renderer) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 1da4746516..389b3c8df8 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -397,6 +397,8 @@ namespace Avalonia.LeakTests public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + public IVisual HitTestFirst(Point p, IVisual root, Func filter) => null; + public void Paint(Rect rect) { } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index cd33fae6f3..d85f6ceac8 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -798,7 +798,7 @@ namespace Avalonia.Markup.UnitTests.Data public OldDataContextTest() { - Bind(BarProperty, this.GetObservable(FooProperty)); + this.Bind(BarProperty, this.GetObservable(FooProperty)); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs index e9c3da5160..b8885369bb 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -35,8 +35,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), - BindingPriority.TemplatedParent)); + It.IsAny>>())); } [Fact] @@ -55,8 +54,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Verify(x => x.Bind( TextBox.TextProperty, - It.IsAny>(), - BindingPriority.TemplatedParent)); + It.IsAny>>())); } private Mock CreateTarget( @@ -66,9 +64,9 @@ namespace Avalonia.Markup.UnitTests.Data var result = new Mock(); result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent); - result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent); - result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text); - result.Setup(x => x.Bind(It.IsAny(), It.IsAny>(), It.IsAny())) + result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent); + result.Setup(x => x.GetValue(TextBox.TextProperty)).Returns(text); + result.Setup(x => x.Bind(It.IsAny>(), It.IsAny>>())) .Returns(Disposable.Empty); return result; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index c3bc649abb..76edf9a17a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -39,6 +39,59 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void BindingExtension_Binds_To_TargetNullValue() + { + using (StyledWindow()) + { + var xaml = @" + + + foobar + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void BindingExtension_TargetNullValue_UnsetByDefault() + { + using (StyledWindow()) + { + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal(false, textBlock.IsVisible); + } + } + + private class FooBar + { + public object Foo { get; } = null; + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 104f46cbac..a5f1eaf96d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.Data; using Avalonia.LogicalTree; using System.Collections.Generic; using System.ComponentModel; @@ -20,10 +21,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml base.OnAttachedToLogicalTree(e); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { - Order.Add($"Property {e.Property.Name} Changed"); - base.OnPropertyChanged(e); + Order.Add($"Property {property.Name} Changed"); + base.OnPropertyChanged(property, oldValue, newValue, priority); } void ISupportInitialize.BeginInit() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs new file mode 100644 index 0000000000..d0cdef3c0b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -0,0 +1,123 @@ +// 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.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ResourceDictionaryTests : XamlTestBase + { + [Fact] + public void StaticResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Finds_Resource_In_Parent_Dictionary() + { + var dictionaryXaml = @" + + +"; + + using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml))) + { + var xaml = @" + + + + + + + + Red + +