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/build/SharedVersion.props b/build/SharedVersion.props index 897e70ff81..bd183faab3 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,7 +3,7 @@ Avalonia 0.9.999 - Copyright 2019 © The AvaloniaUI Project + Copyright 2020 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 677fa2f4c5..4a960d47a1 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -177,14 +177,14 @@ AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown public: virtual HRESULT Initialize() = 0; virtual IAvnMacOptions* GetMacOptions() = 0; - virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) = 0; - virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnPopup** ppv) = 0; + virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; + virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0; virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) = 0; virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; - virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0; + virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; @@ -219,15 +219,12 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetTopMost (bool value) = 0; virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; - virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; - virtual bool TryLock() = 0; - virtual void Unlock() = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -360,24 +357,21 @@ AVNCOM(IAvnCursorFactory, 11) : IUnknown virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) = 0; }; - -AVNCOM(IAvnGlFeature, 12) : IUnknown -{ - virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut) = 0; - virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut) = 0; -}; - AVNCOM(IAvnGlDisplay, 13) : IUnknown { - virtual HRESULT GetSampleCount(int* ret) = 0; - virtual HRESULT GetStencilSize(int* ret) = 0; - virtual HRESULT ClearContext() = 0; + virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) = 0; + virtual void LegacyClearCurrentContext() = 0; + virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) = 0; virtual void* GetProcAddress(char* proc) = 0; }; AVNCOM(IAvnGlContext, 14) : IUnknown { - virtual HRESULT MakeCurrent() = 0; + virtual HRESULT MakeCurrent(IUnknown** ppv) = 0; + virtual HRESULT LegacyMakeCurrent() = 0; + virtual int GetSampleCount() = 0; + virtual int GetStencilSize() = 0; + virtual void* GetNativeHandle() = 0; }; AVNCOM(IAvnGlSurfaceRenderTarget, 15) : IUnknown diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index cf1aa4c735..0ff64b7215 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -162,6 +162,19 @@ public: return _obj; } + TInterface* getRetainedReference() + { + if(_obj == NULL) + return NULL; + _obj->AddRef(); + return _obj; + } + + TInterface** getPPV() + { + return &_obj; + } + operator TInterface*() const { return _obj; diff --git a/native/Avalonia.Native/inc/rendertarget.h b/native/Avalonia.Native/inc/rendertarget.h new file mode 100644 index 0000000000..2b0338d099 --- /dev/null +++ b/native/Avalonia.Native/inc/rendertarget.h @@ -0,0 +1,12 @@ + +@protocol IRenderTarget +-(void) setNewLayer: (CALayer*) layer; +-(HRESULT) setSwFrame: (AvnFramebuffer*) fb; +-(void) resize: (AvnPixelSize) size withScale: (float) scale; +-(AvnPixelSize) pixelSize; +-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget; +@end + +@interface IOSurfaceRenderTarget : NSObject +-(IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context; +@end diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index c0a49382a7..50a85bdf9f 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; + 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; + 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; }; + 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; + 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; @@ -18,7 +22,6 @@ 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; - AB573DC4217605E400D389A2 /* gl.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB573DC3217605E400D389A2 /* gl.mm */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; }; AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; @@ -26,6 +29,10 @@ /* Begin PBXFileReference section */ 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; + 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; }; + 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; + 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; }; + 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; @@ -41,7 +48,6 @@ 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; - AB573DC3217605E400D389A2 /* gl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = gl.mm; sourceTree = ""; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; AB661C1F2148286E00291242 /* window.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = window.mm; sourceTree = ""; }; AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; @@ -54,6 +60,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */, + 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */, AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */, AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */, ); @@ -65,6 +73,8 @@ AB661C1C2148230E00291242 /* Frameworks */ = { isa = PBXGroup; children = ( + 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */, + 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */, AB1E522B217613570091CD71 /* OpenGL.framework */, AB661C1D2148230F00291242 /* AppKit.framework */, ); @@ -78,7 +88,7 @@ 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, - AB573DC3217605E400D389A2 /* gl.mm */, + 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */, 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, @@ -91,6 +101,7 @@ AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, + 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, @@ -180,12 +191,13 @@ 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, + 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, + 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, - AB573DC4217605E400D389A2 /* gl.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/native/Avalonia.Native/src/OSX/cgl.mm b/native/Avalonia.Native/src/OSX/cgl.mm new file mode 100644 index 0000000000..a9d94cdf04 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/cgl.mm @@ -0,0 +1,166 @@ +#include "common.h" +#include + +static CGLContextObj CreateCglContext(CGLContextObj share) +{ + int attributes[] = { + kCGLPFAAccelerated, + kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core, + kCGLPFADepthSize, 8, + kCGLPFAStencilSize, 8, + kCGLPFAColorSize, 32, + 0 + }; + + CGLPixelFormatObj pix; + CGLError errorCode; + GLint num; // stores the number of possible pixel formats + errorCode = CGLChoosePixelFormat( (CGLPixelFormatAttribute*)attributes, &pix, &num ); + if(errorCode != 0) + return nil; + CGLContextObj ctx = nil; + errorCode = CGLCreateContext(pix, share, &ctx ); + CGLDestroyPixelFormat( pix ); + if(errorCode != 0) + return nil; + return ctx; +}; + + + +class AvnGlContext : public virtual ComSingleObject +{ + // Debug + int _usageCount = 0; +public: + CGLContextObj Context; + int SampleCount = 0, StencilBits = 0; + FORWARD_IUNKNOWN() + + class SavedGlContext : public virtual ComUnknownObject + { + CGLContextObj _savedContext; + ComPtr _parent; + public: + SavedGlContext(CGLContextObj saved, AvnGlContext* parent) + { + _savedContext = saved; + _parent = parent; + _parent->_usageCount++; + } + + ~SavedGlContext() + { + if(_parent->Context == CGLGetCurrentContext()) + CGLSetCurrentContext(_savedContext); + _parent->_usageCount--; + CGLUnlockContext(_parent->Context); + } + }; + + AvnGlContext(CGLContextObj context) + { + Context = context; + CGLPixelFormatObj fmt = CGLGetPixelFormat(context); + CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &SampleCount); + CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &StencilBits); + + } + + virtual HRESULT LegacyMakeCurrent() override + { + if(CGLSetCurrentContext(Context) != 0) + return E_FAIL; + return S_OK; + } + + virtual HRESULT MakeCurrent(IUnknown** ppv) override + { + CGLContextObj saved = CGLGetCurrentContext(); + CGLLockContext(Context); + if(CGLSetCurrentContext(Context) != 0) + { + CGLUnlockContext(Context); + return E_FAIL; + } + *ppv = new SavedGlContext(saved, this); + + return S_OK; + } + + virtual int GetSampleCount() override + { + return SampleCount; + } + + virtual int GetStencilSize() override + { + return StencilBits; + } + + virtual void* GetNativeHandle() override + { + return Context; + } + + ~AvnGlContext() + { + CGLReleaseContext(Context); + } +}; + +class AvnGlDisplay : public virtual ComSingleObject +{ + void* _libgl; + +public: + FORWARD_IUNKNOWN() + + AvnGlDisplay() + { + _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY); + } + + virtual void* GetProcAddress(char* proc) override + { + return dlsym(_libgl, proc); + } + + virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) override + { + CGLContextObj shareContext = nil; + if(share != nil) + { + AvnGlContext* shareCtx = dynamic_cast(share); + if(shareCtx != nil) + shareContext = shareCtx->Context; + } + CGLContextObj ctx = ::CreateCglContext(shareContext); + if(ctx == nil) + return E_FAIL; + *ppv = new AvnGlContext(ctx); + return S_OK; + } + + virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) override + { + if(native == nil) + return E_INVALIDARG; + *ppv = new AvnGlContext((CGLContextObj) native); + return S_OK; + } + + virtual void LegacyClearCurrentContext() override + { + CGLSetCurrentContext(nil); + } +}; + +static IAvnGlDisplay* GlDisplay = new AvnGlDisplay(); + + +extern IAvnGlDisplay* GetGlDisplay() +{ + return GlDisplay; +}; + diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 10534dea26..d7eda20f65 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -11,14 +11,13 @@ #include extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events); -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events); +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); -extern IAvnGlFeature* GetGlFeature(); -extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); +extern IAvnGlDisplay* GetGlDisplay(); extern IAvnAppMenu* CreateAppMenu(); extern IAvnAppMenuItem* CreateAppMenuItem(); extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm deleted file mode 100644 index feb0643654..0000000000 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ /dev/null @@ -1,261 +0,0 @@ -#include "common.h" -#include -#include -#include "window.h" - -template char (&ArrayCounter(T (&a)[N]))[N]; -#define ARRAY_COUNT(a) (sizeof(ArrayCounter(a))) - -NSOpenGLPixelFormat* CreateFormat() -{ - NSOpenGLPixelFormatAttribute attribs[] = - { - NSOpenGLPFADoubleBuffer, - NSOpenGLPFAColorSize, 32, - NSOpenGLPFAStencilSize, 8, - NSOpenGLPFADepthSize, 8, - 0 - }; - return [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs]; -} - -class AvnGlContext : public virtual ComSingleObject -{ -public: - FORWARD_IUNKNOWN() - NSOpenGLContext* GlContext; - GLuint Framebuffer, RenderBuffer, StencilBuffer; - AvnGlContext(NSOpenGLContext* gl, bool offscreen) - { - Framebuffer = 0; - RenderBuffer = 0; - StencilBuffer = 0; - GlContext = gl; - if(offscreen) - { - [GlContext makeCurrentContext]; - - glGenFramebuffersEXT(1, &Framebuffer); - glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer); - glGenRenderbuffersEXT(1, &RenderBuffer); - glGenRenderbuffersEXT(1, &StencilBuffer); - - glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer); - } - - } - - - virtual HRESULT MakeCurrent() override - { - [GlContext makeCurrentContext];/* - glBindFramebufferEXT(GL_FRAMEBUFFER, Framebuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, RenderBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, RenderBuffer); - glBindRenderbufferEXT(GL_RENDERBUFFER, StencilBuffer); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, StencilBuffer);*/ - return S_OK; - } -}; - -class AvnGlDisplay : public virtual ComSingleObject -{ - int _sampleCount, _stencilSize; - void* _libgl; - -public: - FORWARD_IUNKNOWN() - - AvnGlDisplay(int sampleCount, int stencilSize) - { - _sampleCount = sampleCount; - _stencilSize = stencilSize; - _libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib", RTLD_LAZY); - } - - virtual HRESULT GetSampleCount(int* ret) override - { - *ret = _sampleCount; - return S_OK; - } - virtual HRESULT GetStencilSize(int* ret) override - { - *ret = _stencilSize; - return S_OK; - } - - virtual HRESULT ClearContext() override - { - [NSOpenGLContext clearCurrentContext]; - return S_OK; - } - - virtual void* GetProcAddress(char* proc) override - { - return dlsym(_libgl, proc); - } -}; - - -class GlFeature : public virtual ComSingleObject -{ - IAvnGlDisplay* _display; - AvnGlContext *_immediate; - NSOpenGLContext* _shared; -public: - FORWARD_IUNKNOWN() - NSOpenGLPixelFormat* _format; - GlFeature(IAvnGlDisplay* display, AvnGlContext* immediate, NSOpenGLPixelFormat* format) - { - _display = display; - _immediate = immediate; - _format = format; - _shared = [[NSOpenGLContext alloc] initWithFormat:_format shareContext:_immediate->GlContext]; - } - - NSOpenGLContext* CreateContext() - { - return _shared; - //return [[NSOpenGLContext alloc] initWithFormat:_format shareContext:nil]; - } - - virtual HRESULT ObtainDisplay(IAvnGlDisplay**retOut) override - { - *retOut = _display; - _display->AddRef(); - return S_OK; - } - - virtual HRESULT ObtainImmediateContext(IAvnGlContext**retOut) override - { - *retOut = _immediate; - _immediate->AddRef(); - return S_OK; - } -}; - -static GlFeature* Feature; - -GlFeature* CreateGlFeature() -{ - auto format = CreateFormat(); - if(format == nil) - { - NSLog(@"Unable to choose pixel format"); - return NULL; - } - - auto immediateContext = [[NSOpenGLContext alloc] initWithFormat:format shareContext:nil]; - if(immediateContext == nil) - { - NSLog(@"Unable to create NSOpenGLContext"); - return NULL; - } - - int stencilBits = 0, sampleCount = 0; - - auto fmt = CGLGetPixelFormat([immediateContext CGLContextObj]); - CGLDescribePixelFormat(fmt, 0, kCGLPFASamples, &sampleCount); - CGLDescribePixelFormat(fmt, 0, kCGLPFAStencilSize, &stencilBits); - - auto offscreen = new AvnGlContext(immediateContext, true); - auto display = new AvnGlDisplay(sampleCount, stencilBits); - - return new GlFeature(display, offscreen, format); -} - - -static GlFeature* GetFeature() -{ - if(Feature == nil) - Feature = CreateGlFeature(); - return Feature; -} - -extern IAvnGlFeature* GetGlFeature() -{ - return GetFeature(); -} - -class AvnGlRenderingSession : public ComSingleObject -{ - AvnView* _view; - AvnWindow* _window; - NSOpenGLContext* _context; -public: - FORWARD_IUNKNOWN() - AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context) - { - _context = context; - _window = window; - _view = view; - } - - virtual HRESULT GetPixelSize(AvnPixelSize* ret) override - { - *ret = [_view getPixelSize]; - return S_OK; - } - virtual HRESULT GetScaling(double* ret) override - { - *ret = [_window getScaling]; - return S_OK; - } - - virtual ~AvnGlRenderingSession() - { - [_context flushBuffer]; - [NSOpenGLContext clearCurrentContext]; - CGLUnlockContext([_context CGLContextObj]); - [_view unlockFocus]; - } -}; - -class AvnGlRenderTarget : public ComSingleObject -{ - NSView* _view; - NSWindow* _window; - NSOpenGLContext* _context; -public: - FORWARD_IUNKNOWN() - AvnGlRenderTarget(NSWindow* window, NSView*view) - { - _window = window; - _view = view; - _context = GetFeature()->CreateContext(); - } - - virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override - { - auto f = GetFeature(); - if(f == NULL) - return E_FAIL; - - @try - { - if(![_view lockFocusIfCanDraw]) - return E_ABORT; - } - @catch(NSException* exception) - { - return E_ABORT; - } - - - auto gl = _context; - CGLLockContext([_context CGLContextObj]); - [gl setView: _view]; - [gl update]; - [gl makeCurrentContext]; - *ret = new AvnGlRenderingSession(_window, _view, gl); - return S_OK; - } -}; - -extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view) -{ - return new AvnGlRenderTarget(window, view); -} diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 9418782fd1..2b6b24cfda 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -174,20 +174,20 @@ public: return (IAvnMacOptions*)new MacOptions(); } - virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) override + virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) override { if(cb == nullptr || ppv == nullptr) return E_POINTER; - *ppv = CreateAvnWindow(cb); + *ppv = CreateAvnWindow(cb, gl); return S_OK; }; - virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv) override + virtual HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) override { if(cb == nullptr || ppv == nullptr) return E_POINTER; - *ppv = CreateAvnPopup(cb); + *ppv = CreateAvnPopup(cb, gl); return S_OK; } @@ -221,9 +221,9 @@ public: return S_OK; } - virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) override + virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) override { - auto rv = ::GetGlFeature(); + auto rv = ::GetGlDisplay(); if(rv == NULL) return E_FAIL; rv->AddRef(); diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm new file mode 100644 index 0000000000..1565417c1a --- /dev/null +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -0,0 +1,284 @@ +#include "common.h" +#include "rendertarget.h" +#import +#import + +#include +#include +#include +#include +#include + +@interface IOSurfaceHolder : NSObject +@end + +@implementation IOSurfaceHolder +{ + @public IOSurfaceRef surface; + @public AvnPixelSize size; + @public float scale; + ComPtr _context; + GLuint _framebuffer, _texture, _renderbuffer; +} + +- (IOSurfaceHolder*) initWithSize: (AvnPixelSize) size + withScale: (float)scale + withOpenGlContext: (IAvnGlContext*) context +{ + long bytesPerRow = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size.Width * 4); + long allocSize = IOSurfaceAlignProperty(kIOSurfaceAllocSize, size.Height * bytesPerRow); + NSDictionary* options = @{ + (id)kIOSurfaceWidth: @(size.Width), + (id)kIOSurfaceHeight: @(size.Height), + (id)kIOSurfacePixelFormat: @((uint)'BGRA'), + (id)kIOSurfaceBytesPerElement: @(4), + (id)kIOSurfaceBytesPerRow: @(bytesPerRow), + (id)kIOSurfaceAllocSize: @(allocSize), + + //(id)kIOSurfaceCacheMode: @(kIOMapWriteCombineCache), + (id)kIOSurfaceElementWidth: @(1), + (id)kIOSurfaceElementHeight: @(1) + }; + + surface = IOSurfaceCreate((CFDictionaryRef)options); + self->scale = scale; + self->size = size; + self->_context = context; + return self; +} + +-(HRESULT) prepareForGlRender +{ + if(_context == nil) + return E_FAIL; + if(CGLGetCurrentContext() != _context->GetNativeHandle()) + return E_FAIL; + if(_framebuffer == 0) + glGenFramebuffersEXT(1, &_framebuffer); + + + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _framebuffer); + if(_texture == 0) + { + glGenTextures(1, &_texture); + + glBindTexture(GL_TEXTURE_RECTANGLE_EXT, _texture); + CGLError res = CGLTexImageIOSurface2D((CGLContextObj)_context->GetNativeHandle(), + GL_TEXTURE_RECTANGLE_EXT, GL_RGBA8, + size.Width, size.Height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface, 0); + glBindTexture(GL_TEXTURE_RECTANGLE_EXT, 0); + + if(res != 0) + { + glDeleteTextures(1, &_texture); + _texture = 0; + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); + return E_FAIL; + } + glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE_EXT, _texture, 0); + } + + if(_renderbuffer == 0) + { + glGenRenderbuffers(1, &_renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, size.Width, size.Height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderbuffer); + } + + return S_OK; +} + +-(void) finishDraw +{ + ComPtr release; + _context->MakeCurrent(release.getPPV()); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); + glFlush(); +} + +-(void) dealloc +{ + + if(_framebuffer != 0) + { + ComPtr release; + _context->MakeCurrent(release.getPPV()); + glDeleteFramebuffers(1, &_framebuffer); + if(_texture != 0) + glDeleteTextures(1, &_texture); + if(_renderbuffer != 0) + glDeleteRenderbuffers(1, &_renderbuffer); + } + IOSurfaceDecrementUseCount(surface); +} +@end + +static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target); + +@implementation IOSurfaceRenderTarget +{ + CALayer* _layer; + @public IOSurfaceHolder* surface; + @public NSObject* lock; + ComPtr _glContext; +} + +- (IOSurfaceRenderTarget*) initWithOpenGlContext: (IAvnGlContext*) context; +{ + self = [super init]; + _glContext = context; + lock = [NSObject new]; + surface = nil; + [self resize:{1,1} withScale: 1]; + + return self; +} + +- (AvnPixelSize) pixelSize { + return {1, 1}; +} + +- (CALayer *)layer { + return _layer; +} + +- (void)resize:(AvnPixelSize)size withScale: (float) scale;{ + @synchronized (lock) { + if(surface == nil + || surface->size.Width != size.Width + || surface->size.Height != size.Height + || surface->scale != scale) + surface = [[IOSurfaceHolder alloc] initWithSize:size withScale:scale withOpenGlContext:_glContext.getRaw()]; + } +} + +- (void)updateLayer { + if ([NSThread isMainThread]) + { + @synchronized (lock) { + if(_layer == nil) + return; + [_layer setContents: nil]; + if(surface != nil) + { + [_layer setContentsScale: surface->scale]; + [_layer setContents: (__bridge IOSurface*) surface->surface]; + } + } + } + else + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateLayer]; + }); +} + +- (void) setNewLayer:(CALayer *)layer { + _layer = layer; + [self updateLayer]; +} + +- (HRESULT)setSwFrame:(AvnFramebuffer *)fb { + @synchronized (lock) { + if(fb->PixelFormat == AvnPixelFormat::kAvnRgb565) + return E_INVALIDARG; + if(surface == nil) + return E_FAIL; + IOSurfaceRef surf = surface->surface; + if(IOSurfaceLock(surf, 0, nil)) + return E_FAIL; + size_t w = MIN(fb->Width, IOSurfaceGetWidth(surf)); + size_t h = MIN(fb->Height, IOSurfaceGetHeight(surf)); + size_t wbytes = w*4; + size_t sstride = IOSurfaceGetBytesPerRow(surf); + size_t fstride = fb->Stride; + char*pSurface = (char*)IOSurfaceGetBaseAddress(surf); + char*pFb = (char*)fb->Data; + for(size_t y = 0; y < h; y++) + { + memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes); + } + IOSurfaceUnlock(surf, 0, nil); + [self updateLayer]; + return S_OK; + } +} + +-(IAvnGlSurfaceRenderTarget*) createSurfaceRenderTarget +{ + return CreateGlRenderTarget(self); +} + +@end + +class AvnGlRenderingSession : public ComSingleObject +{ + ComPtr _releaseContext; + IOSurfaceRenderTarget* _target; + IOSurfaceHolder* _surface; +public: + FORWARD_IUNKNOWN() + AvnGlRenderingSession(IOSurfaceRenderTarget* target, ComPtr releaseContext) + { + _target = target; + // This happens in a synchronized block set up by AvnRenderTarget, so we take the current surface for this + // particular render session + _surface = _target->surface; + _releaseContext = releaseContext; + } + + virtual HRESULT GetPixelSize(AvnPixelSize* ret) override + { + if(!_surface) + return E_FAIL; + *ret = _surface->size; + return S_OK; + } + + virtual HRESULT GetScaling(double* ret) override + { + if(!_surface) + return E_FAIL; + *ret = _surface->scale; + return S_OK; + } + + virtual ~AvnGlRenderingSession() + { + [_surface finishDraw]; + [_target updateLayer]; + _releaseContext = nil; + } +}; + +class AvnGlRenderTarget : public ComSingleObject +{ + IOSurfaceRenderTarget* _target; +public: + FORWARD_IUNKNOWN() + AvnGlRenderTarget(IOSurfaceRenderTarget* target) + { + _target = target; + } + + virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) override + { + ComPtr releaseContext; + @synchronized (_target->lock) { + if(_target->surface == nil) + return E_FAIL; + _target->_glContext->MakeCurrent(releaseContext.getPPV()); + HRESULT res = [_target->surface prepareForGlRender]; + if(res) + return res; + *ret = new AvnGlRenderingSession(_target, releaseContext); + return S_OK; + } + } +}; + + +static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* target) +{ + return new AvnGlRenderTarget(target); +} diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 908b9e1022..b6ce172ffa 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -7,44 +7,9 @@ #include "cursor.h" #include "menu.h" #include +#include "rendertarget.h" + -class SoftwareDrawingOperation -{ -public: - void* Data = 0; - AvnFramebuffer Desc; - void Alloc(NSView* view) - { - auto logicalSize = [view frame].size; - auto pixelSize = [view convertSizeToBacking:logicalSize]; - int w = pixelSize.width; - int h = pixelSize.height; - int stride = w * 4; - Data = malloc(h * stride); - Desc = { - .Data = Data, - .Stride = stride, - .Width = w, - .Height = h, - .PixelFormat = kAvnRgba8888, - .Dpi = AvnVector { .X = w / logicalSize.width * 96, .Y = h / logicalSize.height * 96} - }; - } - - void Dealloc() - { - if(Data != NULL) - { - free(Data); - Data = NULL; - } - } - - ~SoftwareDrawingOperation() - { - Dealloc(); - } -}; class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder { @@ -61,15 +26,18 @@ public: AvnView* View; AvnWindow* Window; ComPtr BaseEvents; - SoftwareDrawingOperation CurrentSwDrawingOperation; + ComPtr _glContext; + NSObject* renderTarget; AvnPoint lastPositionSet; NSString* _lastTitle; IAvnAppMenu* _mainMenu; - WindowBaseImpl(IAvnWindowBaseEvents* events) + WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) { _mainMenu = nullptr; BaseEvents = events; + _glContext = gl; + renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; Window = [[AvnWindow alloc] initWithParent:this]; @@ -291,29 +259,6 @@ public: return S_OK; } - virtual bool TryLock() override - { - @autoreleasepool - { - @try - { - return [View lockFocusIfCanDraw] == YES; - } - @catch (NSException*) - { - return NO; - } - } - } - - virtual void Unlock() override - { - @autoreleasepool - { - [View unlockFocus]; - } - } - virtual HRESULT BeginMoveDrag () override { @autoreleasepool @@ -408,16 +353,6 @@ public: return S_OK; } - virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) override - { - if(![[NSThread currentThread] isMainThread]) - return E_FAIL; - if(CurrentSwDrawingOperation.Data == NULL) - CurrentSwDrawingOperation.Alloc(View); - *ret = CurrentSwDrawingOperation.Desc; - return S_OK; - } - virtual HRESULT SetCursor(IAvnCursor* cursor) override { @autoreleasepool @@ -451,8 +386,8 @@ public: { if(View == NULL) return E_FAIL; - *ppv = ::CreateGlRenderTarget(Window, View); - return S_OK; + *ppv = [renderTarget createSurfaceRenderTarget]; + return *ppv == nil ? E_FAIL : S_OK; } protected: @@ -490,7 +425,7 @@ private: } ComPtr WindowEvents; - WindowImpl(IAvnWindowEvents* events) : WindowBaseImpl(events) + WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { WindowEvents = events; [Window setCanBecomeKeyAndMain]; @@ -731,6 +666,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; AvnPixelSize _lastPixelSize; + NSObject* _renderTarget; } - (void)onClosed @@ -741,19 +677,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -- (BOOL)lockFocusIfCanDraw -{ - @synchronized (self) - { - if(_parent == nullptr) - { - return NO; - } - } - - return [super lockFocusIfCanDraw]; -} - -(AvnPixelSize) getPixelSize { return _lastPixelSize; @@ -764,18 +687,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return _lastMouseDownEvent; } +- (void) updateRenderTarget +{ + [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]]; + [self setNeedsDisplayInRect:[self frame]]; +} + -(AvnView*) initWithParent: (WindowBaseImpl*) parent { self = [super init]; - [self setWantsBestResolutionOpenGLSurface:true]; + _renderTarget = parent->renderTarget; [self setWantsLayer:YES]; + [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; + _parent = parent; _area = nullptr; _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; + return self; } +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)setLayer:(CALayer *)layer +{ + [_renderTarget setNewLayer: layer]; + [super setLayer: layer]; +} + - (BOOL)isOpaque { return YES; @@ -805,7 +753,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self removeTrackingArea:_area]; _area = nullptr; } - + + if (_parent == nullptr) + { + return; + } + NSRect rect = NSZeroRect; rect.size = newSize; @@ -818,87 +771,32 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto fsize = [self convertSizeToBacking: [self frame].size]; _lastPixelSize.Width = (int)fsize.width; _lastPixelSize.Height = (int)fsize.height; - + [self updateRenderTarget]; _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } -- (void) drawFb: (AvnFramebuffer*) fb -{ - auto colorSpace = CGColorSpaceCreateDeviceRGB(); - auto dataProvider = CGDataProviderCreateWithData(NULL, fb->Data, fb->Height*fb->Stride, NULL); - - - auto image = CGImageCreate(fb->Width, fb->Height, 8, 32, fb->Stride, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast, - dataProvider, nullptr, false, kCGRenderingIntentDefault); - - auto ctx = [NSGraphicsContext currentContext]; - - [ctx saveGraphicsState]; - auto cgc = [ctx CGContext]; - - CGContextDrawImage(cgc, CGRect{0,0, fb->Width/(fb->Dpi.X/96), fb->Height/(fb->Dpi.Y/96)}, image); - CGImageRelease(image); - CGColorSpaceRelease(colorSpace); - CGDataProviderRelease(dataProvider); - - [ctx restoreGraphicsState]; - -} -- (void)drawRect:(NSRect)dirtyRect +- (void)updateLayer { if (_parent == nullptr) { return; } - - _parent->BaseEvents->RunRenderPriorityJobs(); - - @synchronized (self) { - if(_swRenderedFrame != NULL) - { - [self drawFb: &_swRenderedFrameBuffer]; - return; - } - } - auto swOp = &_parent->CurrentSwDrawingOperation; + _parent->BaseEvents->RunRenderPriorityJobs(); _parent->BaseEvents->Paint(); - if(swOp->Data != NULL) - [self drawFb: &swOp->Desc]; - - swOp->Dealloc(); - return; } --(void) redrawSelf +- (void)drawRect:(NSRect)dirtyRect { - @autoreleasepool - { - @synchronized(self) - { - if(!_queuedDisplayFromThread) - return; - _queuedDisplayFromThread = false; - } - [self setNeedsDisplayInRect:[self frame]]; - [self display]; - - } + return; } -(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose { @autoreleasepool { - @synchronized (self) { - _swRenderedFrame = dispose; - _swRenderedFrameBuffer = *fb; - if(!_queuedDisplayFromThread) - { - _queuedDisplayFromThread = true; - [self performSelector:@selector(redrawSelf) onThread:[NSThread mainThread] withObject:NULL waitUntilDone:false modes: AllLoopModes]; - } - } + [_renderTarget setSwFrame:fb]; + dispose->Release(); } } @@ -923,7 +821,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto fsize = [self convertSizeToBacking: [self frame].size]; _lastPixelSize.Width = (int)fsize.width; _lastPixelSize.Height = (int)fsize.height; - + [self updateRenderTarget]; _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); [super viewDidChangeBackingProperties]; @@ -1283,6 +1181,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _closed = false; _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } @@ -1470,7 +1371,7 @@ private: END_INTERFACE_MAP() virtual ~PopupImpl(){} ComPtr WindowEvents; - PopupImpl(IAvnWindowEvents* events) : WindowBaseImpl(events) + PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; @@ -1494,20 +1395,20 @@ protected: } }; -extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events) +extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool { - IAvnPopup* ptr = dynamic_cast(new PopupImpl(events)); + IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); return ptr; } } -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events) +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool { - IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events); + IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); return ptr; } } 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..42b1e52205 100644 --- a/readme.md +++ b/readme.md @@ -10,19 +10,23 @@ **Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). | Control catalog | Desktop platforms | Mobile platforms | |---|---|---| | | | | +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources. + ## Getting Started Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! 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)) +If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. + +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/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index 39bbf391bb..13bae59805 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -6,15 +6,19 @@ A progress bar control + - + - + 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..b0ff591682 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)); @@ -36,7 +34,6 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -57,15 +54,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 +71,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 +134,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 +224,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 +249,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 +299,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 +345,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 +382,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); - return Bind(property, source.Select(x => (object)x), priority); + 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 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(); + } + + 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 (!Equals(oldValue, 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 +477,65 @@ namespace Avalonia (BindingPriority)priority); } } - - internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) - { - LogIfError(property, notification); - UpdateDataValidation(property, notification); - } - /// - Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { - return _propertyChanged?.GetInvocationList(); + ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); } /// - /// 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 +559,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 +588,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 +667,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) - { - return TypeUtilities.ConvertImplicitOrDefault(value, type); - } - else + if (property.Inherits && InheritanceParent is AvaloniaObject o) { - if (notification.HasValue) - { - notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type)); - } - - return notification; + return o.GetValueOrInheritedOrDefault(property); } - } - /// - /// 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()); + return 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 +772,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 +782,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 +796,7 @@ namespace Avalonia } else { - LogBindingError(property, notification.Error); + LogBindingError(property, value.Error); } } } @@ -814,7 +807,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 +818,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 +843,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..aa7a675764 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. @@ -22,7 +20,6 @@ namespace Avalonia public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; - private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; @@ -55,7 +52,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _initialized = new Subject(); _changed = new Subject(); _metadata = new Dictionary(); @@ -83,7 +79,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _initialized = source._initialized; _changed = source._changed; _metadata = new Dictionary(); @@ -138,22 +133,6 @@ namespace Avalonia /// public virtual bool IsReadOnly => false; - /// - /// Gets an observable that is fired when this property is initialized on a - /// new instance. - /// - /// - /// This observable is fired each time a new is constructed - /// for all properties registered on the object's type. The default value of the property - /// for the object is passed in the args' NewValue (OldValue will always be - /// . - /// - /// - /// An observable that is fired when this property is initialized on a new - /// instance. - /// - public IObservable Initialized => _initialized; - /// /// Gets an observable that is fired when this property changes on any /// instance. @@ -183,6 +162,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 +236,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 +249,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 +258,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 +282,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 +318,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 +327,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 +353,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 +369,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; } @@ -478,20 +469,6 @@ namespace Avalonia return Name; } - /// - /// True if has any observers. - /// - internal bool HasNotifyInitializedObservers => _initialized.HasObservers; - - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e) - { - _initialized.OnNext(e); - } - /// /// Notifies the observable. /// @@ -501,6 +478,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 +561,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..29ab10278b 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); @@ -320,70 +415,6 @@ namespace Avalonia _inheritedCache.Clear(); } - internal void NotifyInitialized(AvaloniaObject o) - { - Contract.Requires(o != null); - - 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(); - - initializationData = new List(); - - foreach (AvaloniaProperty property in GetRegistered(type)) - { - if (property.IsDirect) - { - initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); - } - else - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - } - - visited.Add(property); - } - - foreach (AvaloniaProperty property in GetRegisteredAttached(type)) - { - if (!visited.Contains(property)) - { - initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); - - visited.Add(property); - } - } - - _initializedCache.Add(type, initializationData); - } - - foreach (PropertyInitializationData data in initializationData) - { - if (!data.Property.HasNotifyInitializedObservers) - { - continue; - } - - object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value; - - Notify(data.Property, value); - } - } - private readonly struct PropertyInitializationData { public AvaloniaProperty Property { get; } 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/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 3c8d4ca7e6..ca51822eed 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -327,6 +327,8 @@ namespace Avalonia.Collections } else { + EnsureCapacity(_inner.Count + list.Count); + using (IEnumerator en = items.GetEnumerator()) { int insertIndex = index; @@ -550,6 +552,24 @@ namespace Avalonia.Collections /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); + private void EnsureCapacity(int capacity) + { + // Adapted from List implementation. + var currentCapacity = _inner.Capacity; + + if (currentCapacity < capacity) + { + var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + _inner.Capacity = newCapacity; + } + } + /// /// Raises the event with an add action. /// 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..39ed3b084f --- /dev/null +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -0,0 +1,153 @@ +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 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..3249b31d66 --- /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, Value); + } + + public void OnCompleted() => _sink.Completed(Property, this, Value); + + 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..9012a985ac --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -0,0 +1,23 @@ +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( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue); + } +} 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..4ef8f650fa --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -0,0 +1,195 @@ +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( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) + { + _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..d1f961a567 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,31 +155,18 @@ namespace Avalonia /// The metadata. public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) { - base.OverrideMetadata(type, metadata); - } - - /// - /// Overrides the validation function for the specified type. - /// - /// The type. - /// The validation function. - public void OverrideValidation(Func validate) - where THost : IAvaloniaObject - { - Func f; - - if (validate != null) - { - f = Cast(validate); - } - else + if (ValidateValue != null) { - // 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; + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } } - base.OverrideMetadata(typeof(THost), new StyledPropertyMetadata(validate: f)); + HasCoercion |= metadata.CoerceValue != null; + + base.OverrideMetadata(type, metadata); } /// @@ -157,20 +179,65 @@ namespace Avalonia } /// - Func IStyledPropertyAccessor.GetValidationFunc(Type type) + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + /// + internal override void RouteClearValue(IAvaloniaObject o) { - Contract.Requires(type != null); - return ((IStyledPropertyMetadata)base.GetMetadata(type)).Validate; + o.ClearValue(this); } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + 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, priority); + } + 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, 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/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 37e25d0fac..aca08f5259 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -183,11 +183,16 @@ namespace Avalonia.Utilities for (int c = 0; c < _count; c++) { var r = _data[c]; + + TSubscriber target = null; + + r.Subscriber?.TryGetTarget(out target); + //Mark current index as first empty - if (r.Subscriber == null && empty == -1) + if (target == null && empty == -1) empty = c; //If current element isn't null and we have an empty one - if (r.Subscriber != null && empty != -1) + if (target != null && empty != -1) { _data[c] = default; _data[empty] = r; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e06c5996c9..e9118af9f1 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,205 +1,276 @@ 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.Unset, + 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( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { - 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); + _sink.Completed(property, entry, oldValue); + } } - - 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); + priorityValue.SetValue(value, priority); + _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..214132f03c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -149,6 +149,9 @@ namespace Avalonia.Controls private IEnumerable _items; + public event EventHandler HorizontalScroll; + public event EventHandler VerticalScroll; + /// /// Identifies the CanUserReorderColumns dependency property. /// @@ -202,20 +205,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 +268,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)); @@ -389,36 +376,22 @@ namespace Avalonia.Controls public bool IsValid { get { return _isValid; } - internal set { SetAndRaise(IsValidProperty, ref _isValid, value); } + internal set + { + SetAndRaise(IsValidProperty, ref _isValid, value); + PseudoClasses.Set(":invalid", !value); + } } public static readonly StyledProperty MaxColumnWidthProperty = 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 +407,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 +439,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 +460,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); } /// @@ -721,8 +663,6 @@ namespace Avalonia.Controls HorizontalScrollBarVisibilityProperty, VerticalScrollBarVisibilityProperty); - PseudoClass(IsValidProperty, x => !x, ":invalid"); - ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); @@ -2537,25 +2477,25 @@ namespace Avalonia.Controls internal bool ProcessDownKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessDownKeyInternal(shift, ctrl); } internal bool ProcessEndKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEndKey(shift, ctrl); } internal bool ProcessEnterKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessEnterKey(shift, ctrl); } internal bool ProcessHomeKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessHomeKey(shift, ctrl); } @@ -2595,25 +2535,25 @@ namespace Avalonia.Controls internal bool ProcessLeftKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessLeftKey(shift, ctrl); } internal bool ProcessNextKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessNextKey(shift, ctrl); } internal bool ProcessPriorKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessPriorKey(shift, ctrl); } internal bool ProcessRightKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessRightKey(shift, ctrl); } @@ -2731,7 +2671,7 @@ namespace Avalonia.Controls internal bool ProcessUpKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessUpKey(shift, ctrl); } @@ -2999,7 +2939,7 @@ namespace Avalonia.Controls //TODO: Ensure left button is checked for internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit) { - KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.InputModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift); return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl); } @@ -4288,6 +4228,7 @@ namespace Avalonia.Controls private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e) { ProcessHorizontalScroll(e.ScrollEventType); + HorizontalScroll?.Invoke(sender, e); } private bool IsColumnOutOfBounds(int columnIndex) @@ -4441,7 +4382,7 @@ namespace Avalonia.Controls private bool ProcessAKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift, out bool alt); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift, out bool alt); if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended) { @@ -4507,10 +4448,10 @@ namespace Avalonia.Controls return ProcessAKey(e); case Key.C: - return ProcessCopyKey(e.Modifiers); + return ProcessCopyKey(e.KeyModifiers); case Key.Insert: - return ProcessCopyKey(e.Modifiers); + return ProcessCopyKey(e.KeyModifiers); } if (focusDataGrid) { @@ -4709,7 +4650,7 @@ namespace Avalonia.Controls private bool ProcessF2Key(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); if (!shift && !ctrl && _editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) && @@ -5066,7 +5007,7 @@ namespace Avalonia.Controls private bool ProcessTabKey(KeyEventArgs e) { - KeyboardHelper.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(e.KeyModifiers, out bool ctrl, out bool shift); return ProcessTabKey(e, shift, ctrl); } @@ -5620,6 +5561,7 @@ namespace Avalonia.Controls private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e) { ProcessVerticalScroll(e.ScrollEventType); + VerticalScroll?.Invoke(sender, e); } //TODO: Ensure left button is checked for @@ -5852,7 +5794,7 @@ namespace Avalonia.Controls /// to the Clipboard as text. /// /// Whether or not the DataGrid handled the key press. - private bool ProcessCopyKey(InputModifiers modifiers) + private bool ProcessCopyKey(KeyModifiers modifiers) { KeyboardHelper.GetMetaKeyState(modifiers, out bool ctrl, out bool shift, out bool alt); diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index d1651b2d09..bff2b500ae 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -190,28 +190,28 @@ namespace Avalonia.Controls } } - internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled) + internal void OnMouseLeftButtonUp_Click(KeyModifiers keyModifiers, ref bool handled) { // completed a click without dragging, so we're sorting - InvokeProcessSort(inputModifiers); + InvokeProcessSort(keyModifiers); handled = true; } - internal void InvokeProcessSort(InputModifiers inputModifiers) + internal void InvokeProcessSort(KeyModifiers keyModifiers) { Debug.Assert(OwningGrid != null); - if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(inputModifiers))) + if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(inputModifiers)); + Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); } } //TODO GroupSorting - internal void ProcessSort(InputModifiers inputModifiers) + internal void ProcessSort(KeyModifiers keyModifiers) { // if we can sort: // - DataConnection.AllowSort is true, and @@ -233,7 +233,7 @@ namespace Avalonia.Controls DataGridSortDescription newSort; - KeyboardHelper.GetMetaKeyState(inputModifiers, out bool ctrl, out bool shift); + KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); DataGridSortDescription sort = OwningColumn.GetSortDescription(); IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView; @@ -326,7 +326,7 @@ namespace Avalonia.Controls if (OwningGrid != null && OwningGrid.ColumnHeaders != null) { - args.Device.Capture(this); + args.Pointer.Capture(this); _dragMode = DragMode.MouseDown; _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); @@ -371,7 +371,7 @@ namespace Avalonia.Controls { if (_dragMode == DragMode.MouseDown) { - OnMouseLeftButtonUp_Click(args.InputModifiers, ref handled); + OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled); } else if (_dragMode == DragMode.Reorder) { @@ -391,7 +391,7 @@ namespace Avalonia.Controls SetDragCursor(mousePosition); // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture - args.Device.Capture(null); + args.Pointer.Capture(null); OnLostMouseCapture(); _dragMode = DragMode.None; handled = true; 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.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 7b6870fec3..17e7ecba43 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -45,7 +45,7 @@ - + + + + diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index f3def542fc..8bb0fc297c 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -1,10 +1,15 @@ diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 59c596bcaa..e91d8a6772 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -1,4 +1,5 @@ - + + + diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 72271e785a..d3c2f0c784 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,14 +1,25 @@ @@ -22,42 +33,49 @@ + diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index eb46fdffa2..b21cbf3650 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -4,7 +4,7 @@ - + @@ -20,7 +20,7 @@ - + @@ -55,7 +55,7 @@ - + @@ -87,4 +87,7 @@ + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index ec1f6e9e7d..2c4cafde26 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -12,7 +12,9 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + + 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..bd2ab6e614 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -386,11 +386,18 @@ namespace Avalonia AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnAttachedToVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnAttachedToVisualTreeCore(e); + } } } } @@ -415,11 +422,18 @@ namespace Avalonia DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnDetachedFromVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnDetachedFromVisualTreeCore(e); + } } } } @@ -447,7 +461,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.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs index eaca9ed9d7..1e0662c17a 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Reactive.Linq; -using System.Reflection; using Avalonia.Reactive; namespace Avalonia.VisualTree @@ -50,7 +48,7 @@ namespace Avalonia.VisualTree if (_relativeTo.IsAttachedToVisualTree) { return _relativeTo.GetVisualAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .Where(x => _ancestorType?.IsAssignableFrom(x.GetType()) ?? true) .ElementAtOrDefault(_ancestorLevel); } else diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index 45a46bd6f5..c49903cc16 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -80,6 +80,7 @@ namespace Avalonia.X11.Glx public IGlDisplay Display => _context.Display; public PixelSize Size => _info.Size; public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } } } } 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..94f1dbc8d1 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 @@ -991,11 +995,18 @@ namespace Avalonia.X11 public void SetIcon(IWindowIconImpl icon) { - var data = ((X11IconData)icon).Data; - fixed (void* pdata = data) - XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON, - new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace, - pdata, data.Length); + if (icon != null) + { + var data = ((X11IconData)icon).Data; + fixed (void* pdata = data) + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON, + new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace, + pdata, data.Length); + } + else + { + XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON); + } } public void ShowTaskbarIcon(bool value) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 273265a6dc..1ebfd1a47f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -234,6 +234,8 @@ namespace Avalonia.LinuxFramebuffer.Output public PixelSize Size => _parent._mode.Resolution; public double Scaling => _parent.Scaling; + + public bool IsYFlipped { get; } } public IGlPlatformSurfaceRenderingSession BeginDraw() 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/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index c81a3718a7..d56435b4f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -28,7 +27,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 0e2a131afd..0e95af674b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -2,12 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Data; using Avalonia.Markup.Parsers; using Avalonia.Metadata; @@ -34,7 +31,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } 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/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index 61ccf09e52..10d8ff3234 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -54,7 +54,7 @@ namespace Avalonia.Skia new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); var surface = SKSurface.Create(_grContext, renderTarget, - GRSurfaceOrigin.BottomLeft, + session.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, GRPixelConfig.Rgba8888.ToColorType()); var nfo = new DrawingContextImpl.CreateInfo diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 05c3bbdaa0..65ed1f506e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -43,6 +43,7 @@ namespace Avalonia.Skia { GrContext = GRContext.Create(GRBackend.OpenGL, iface); } + display.ClearContext(); } } 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..cea9f88799 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; } @@ -942,7 +942,7 @@ namespace Avalonia.Win32 public void SetIcon(IWindowIconImpl icon) { var impl = (IconImpl)icon; - var hIcon = impl.HIcon; + var hIcon = impl?.HIcon ?? IntPtr.Zero; UnmanagedMethods.PostMessage(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_SETICON, new IntPtr((int)UnmanagedMethods.Icons.ICON_BIG), hIcon); } 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..474ef9abec 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,27 +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() - { - bool raised = false; - - using (Class1.FooProperty.Initialized.Subscribe(x => raised = true)) - { - new Class3(); - } - - Assert.True(raised); - } - private class Base : AvaloniaObject { } @@ -46,18 +25,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 @@ -65,9 +33,5 @@ namespace Avalonia.Base.UnitTests public static readonly AttachedProperty FooProperty = Class1.FooProperty.AddOwner(); } - - private class Class3 : Base - { - } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 23984a7c8d..2839fde320 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,294 @@ 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 Completing_LocalValue_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Completing_Style_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, source, BindingPriority.Style); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Completing_LocalValue_Binding_With_Style_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, new BehaviorSubject("bar"), BindingPriority.Style); + target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Style, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("bar", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("bar", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Disposing_LocalValue_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + var sub = target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + sub.Dispose(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [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 +404,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 +469,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 +580,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("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)); + } - Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + [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 +653,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 +664,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 +723,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 +754,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..ca17afb94f 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,36 @@ 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(BindingPriority.LocalValue, e.Priority); + 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 +253,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 +277,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(45); - Assert.Null(target.Foo); + Assert.Equal("unset", target.Foo); } [Fact] @@ -207,7 +290,7 @@ namespace Avalonia.Base.UnitTests source.OnNext("foo"); - Assert.Equal(0, target.Baz); + Assert.Equal(-1, target.Baz); } [Fact] @@ -343,46 +426,66 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Property_Notifies_Initialized() + public void Binding_Error_Reverts_To_Default_Value() { - bool raised = false; + var target = new Class1(); + var source = new Subject>(); - Class1.FooProperty.Initialized.Subscribe(e => - raised = e.Property == Class1.FooProperty && - e.OldValue == AvaloniaProperty.UnsetValue && - (string)e.NewValue == "initial" && - e.Priority == BindingPriority.Unset); + 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>(); - Assert.True(raised); + 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 +494,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 +515,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 +550,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 +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", defaultBindingMode: BindingMode.OneWayToSource); Assert.Equal(BindingMode.TwoWay, bar.GetMetadata().DefaultBindingMode); @@ -527,10 +632,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 +667,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 +728,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..4b477287e8 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -20,6 +20,28 @@ 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(BindingPriority.Unset, e.Priority); + 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 +81,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 +218,39 @@ 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 SetValue_Animation_Overrides_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.Animation); + Assert.Equal("two", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Setting_UnsetValue_Reverts_To_Default_Value() { @@ -188,10 +262,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..6bb8dfe1f5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -78,24 +78,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode); } - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("default", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void Changed_Observable_Fired() { @@ -140,6 +122,37 @@ namespace Avalonia.Base.UnitTests { OverrideMetadata(typeof(T), metadata); } + + 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..8f62dc9136 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -1,41 +1,21 @@ // 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 DirectPropertyTests { - [Fact] - public void Initialized_Observable_Fired() - { - bool invoked = false; - - Class1.FooProperty.Initialized.Subscribe(x => - { - Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue); - Assert.Equal("foo", x.NewValue); - Assert.Equal(BindingPriority.Unset, x.Priority); - invoked = true; - }); - - var target = new Class1(); - - Assert.True(invoked); - } - [Fact] public void IsDirect_Property_Returns_True() { var target = new DirectProperty( "test", o => null, - null, - new DirectPropertyMetadata()); + null, + new DirectPropertyMetadata(), + false); Assert.True(target.IsDirect); } @@ -68,18 +48,6 @@ namespace Avalonia.Base.UnitTests var p2 = p1.AddOwner(o => null, (o, v) => { }); Assert.Same(p1.Changed, p2.Changed); - 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 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/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs index 06716f7102..d2e38850f2 100644 --- a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base [MemoryDiagnoser] public class AvaloniaObjectInitializationBenchmark { - [Benchmark(OperationsPerInvoke = 1000)] + [Benchmark] public Button InitializeButton() { return new Button(); 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/Layout/CalendarBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs new file mode 100644 index 0000000000..c55912d94d --- /dev/null +++ b/tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Layout +{ + [MemoryDiagnoser] + public class CalendarBenchmark : IDisposable + { + private readonly IDisposable _app; + private readonly TestRoot _root; + + public CalendarBenchmark() + { + _app = UnitTestApplication.Start( + TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform())); + + _root = new TestRoot(true, null) + { + Renderer = new NullRenderer() + }; + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateCalendar() + { + var calendar = new Calendar(); + + _root.Child = calendar; + + _root.LayoutManager.ExecuteLayoutPass(); + } + + public void Dispose() + { + _app.Dispose(); + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs b/tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs new file mode 100644 index 0000000000..f886d077cc --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Benchmarks +{ + internal class NullFormattedTextImpl : IFormattedTextImpl + { + public Size Constraint { get; } + + public Rect Bounds { get; } + + public string Text { get; } + + public IEnumerable GetLines() + { + throw new NotImplementedException(); + } + + public TextHitTestResult HitTestPoint(Point point) + { + throw new NotImplementedException(); + } + + public Rect HitTestTextPosition(int index) + { + throw new NotImplementedException(); + } + + public IEnumerable HitTestTextRange(int index, int length) + { + throw new NotImplementedException(); + } + } +} 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/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs new file mode 100644 index 0000000000..101d40f00a --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; + +namespace Avalonia.Benchmarks +{ + internal class NullRenderingPlatform : IPlatformRenderInterface + { + public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, + TextWrapping wrapping, Size constraint, IReadOnlyList spans) + { + return new NullFormattedTextImpl(); + } + + public IGeometryImpl CreateEllipseGeometry(Rect rect) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateLineGeometry(Point p1, Point p2) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateRectangleGeometry(Rect rect) + { + throw new NotImplementedException(); + } + + public IStreamGeometryImpl CreateStreamGeometry() + { + return new MockStreamGeometryImpl(); + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + throw new NotImplementedException(); + } + + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) + { + throw new NotImplementedException(); + } + + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmap(string fileName) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmap(Stream stream) + { + throw new NotImplementedException(); + } + + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) + { + throw new NotImplementedException(); + } + + public IFontManagerImpl CreateFontManager() + { + return new MockFontManagerImpl(); + } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs new file mode 100644 index 0000000000..ba84b5afcc --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs @@ -0,0 +1,28 @@ +using System; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Benchmarks +{ + internal class NullThreadingPlatform : IPlatformThreadingInterface + { + public void RunLoop(CancellationToken cancellationToken) + { + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + return Disposable.Empty; + } + + public void Signal(DispatcherPriority priority) + { + } + + public bool CurrentThreadIsLoopThread => true; + + public event Action Signaled; + } +} 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.Benchmarks/Styling/StyleAttachBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs new file mode 100644 index 0000000000..7bccd65c81 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class StyleAttachBenchmark : IDisposable + { + private readonly IDisposable _app; + private readonly TestRoot _root; + private readonly TextBox _control; + + public StyleAttachBenchmark() + { + _app = UnitTestApplication.Start( + TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform())); + + _root = new TestRoot(true, null) + { + Renderer = new NullRenderer(), + }; + + _control = new TextBox(); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void AttachTextBoxStyles() + { + var styles = UnitTestApplication.Current.Styles; + + styles.Attach(_control, UnitTestApplication.Current); + + styles.Detach(); + } + + public void Dispose() + { + _app.Dispose(); + } + } +} 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/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index b16ac6bb8e..1bcae8e736 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -51,10 +51,9 @@ namespace Avalonia.Controls.UnitTests Assert.Single(target.GetLogicalChildren()); - var child = target.GetLogicalChildren().Single(); + var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.Equal("Foo", child.Text); } [Fact] @@ -98,20 +97,18 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(3, target.GetLogicalChildren().Count()); - var child = target.GetLogicalChildren().First(); + var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.Equal("Foo", child.Text); var newItems = items.ToList(); newItems.RemoveAt(0); target.Items = newItems; - child = target.GetLogicalChildren().First(); + child = GetContainerTextBlock(target.GetLogicalChildren().First()); - Assert.IsType(child); - Assert.Equal("Bar", ((TextBlock)child).Text); + Assert.Equal("Bar", child.Text); } [Fact] @@ -136,20 +133,18 @@ namespace Avalonia.Controls.UnitTests Assert.Single(target.GetLogicalChildren()); - var child = target.GetLogicalChildren().Single(); + var child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.Equal("Foo", child.Text); var newItems = items.ToList(); newItems.RemoveAt(0); target.Items = newItems; - child = target.GetLogicalChildren().Single(); + child = GetContainerTextBlock(target.GetLogicalChildren().Single()); - Assert.IsType(child); - Assert.Equal("Bar", ((TextBlock)child).Text); + Assert.Equal("Bar", child.Text); } [Fact] @@ -197,10 +192,9 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(3, target.GetLogicalChildren().Count()); - var child = target.GetLogicalChildren().First(); + var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.Equal("Foo", child.Text); target.Items = null; @@ -233,7 +227,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("FooBar", target.SelectedItem); - var child = target.GetVisualDescendants().LastOrDefault(); + var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault()); Assert.IsType(child); Assert.Equal("FooBar", ((TextBlock)child).Text); @@ -261,14 +255,13 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(3, target.GetLogicalChildren().Count()); - var child = target.GetLogicalChildren().First(); + var child = GetContainerTextBlock(target.GetLogicalChildren().First()); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.Equal("Foo", child.Text); items.RemoveAt(0); - child = target.GetLogicalChildren().First(); + child = GetContainerTextBlock(target.GetLogicalChildren().First()); Assert.IsType(child); Assert.Equal("Bar", ((TextBlock)child).Text); @@ -314,5 +307,12 @@ namespace Avalonia.Controls.UnitTests [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], }.RegisterInNameScope(scope); } + + private static TextBlock GetContainerTextBlock(object control) + { + var contentPresenter = Assert.IsType(control); + contentPresenter.UpdateChild(); + return Assert.IsType(contentPresenter.Child); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 599e214b31..81908b5147 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -108,5 +108,36 @@ namespace Avalonia.Controls.UnitTests }; }); } + + [Fact] + public void Detaching_Closed_ComboBox_Keeps_Current_Focus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target = new ComboBox + { + Items = new[] { new Canvas() }, + SelectedIndex = 0, + Template = GetTemplate(), + }; + + var other = new Control { Focusable = true }; + + StackPanel panel; + + var root = new TestRoot { Child = panel = new StackPanel { Children = { target, other } } }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + other.Focus(); + + Assert.True(other.IsFocused); + + panel.Children.Remove(target); + + Assert.True(other.IsFocused); + } + } } } 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/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 227d783874..a728500316 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Container_Child_Should_Have_LogicalParent_Set_To_Container() + public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -87,7 +87,7 @@ namespace Avalonia.Controls.UnitTests var container = (ContentPresenter)target.Presenter.Panel.Children[0]; - Assert.Equal(container, container.Child.Parent); + Assert.Equal(target, container.Parent); } } @@ -190,7 +190,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Adding_String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren() + public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren() { var target = new ItemsControl(); var child = new Control(); @@ -202,7 +202,7 @@ namespace Avalonia.Controls.UnitTests var logical = (ILogical)target; Assert.Equal(1, logical.LogicalChildren.Count); - Assert.IsType(logical.LogicalChildren[0]); + Assert.IsType(logical.LogicalChildren[0]); } [Fact] @@ -575,6 +575,30 @@ namespace Avalonia.Controls.UnitTests }; } + [Fact] + public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw() + { + // # Issue 3487 + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new[] { "foo", "bar" }, + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + }; + + var root = new TestRoot(target); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + root.Child = null; + root.Child = target; + + target.Measure(Size.Infinity); + + root.Child = null; + root.Child = target; + } + private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 989bd744a6..97b6bf9d24 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -199,6 +199,18 @@ namespace Avalonia.Controls.UnitTests.Platform menu.VerifySet(x => x.SelectedItem = null, Times.Never); Assert.False(e.Handled); } + + [Fact] + public void Doesnt_Throw_On_Menu_Keypress() + { + // Issue #3459 + var target = new DefaultMenuInteractionHandler(false); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Tab, Source = menu }; + + target.KeyDown(menu, e); + } } public class NonTopLevel diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 59f3ae44c2..f7942a017d 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -60,7 +60,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); parentMock.As(); - parentMock.As(); + parentMock.As(); (target as ISetLogicalParent).SetParent(parentMock.Object); @@ -105,7 +105,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); - parentMock.As(); + parentMock.As(); parentMock.As().SetupGet(l => l.IsAttachedToLogicalTree).Returns(true); (contentControl as ISetLogicalParent).SetParent(parentMock.Object); @@ -150,7 +150,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); parentMock.As(); - parentMock.As(); + parentMock.As(); (target as ISetLogicalParent).SetParent(parentMock.Object); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 7a6cf0fba7..308ab4b2b5 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -13,6 +13,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; @@ -1062,7 +1063,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, IStyleRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, ILogicalRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } 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/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 7cb9fccee8..1f07482617 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -223,7 +223,33 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - + [Fact] + public void Popup_Close_On_Closed_Popup_Should_Not_Raise_Closed_Event() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() { PlacementMode = PlacementMode.Pointer }; + + window.Content = target; + window.ApplyTemplate(); + + int closedCount = 0; + + target.Closed += (sender, args) => + { + closedCount++; + }; + + target.Close(); + target.Close(); + target.Close(); + target.Close(); + + Assert.Equal(0, closedCount); + } + } + [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d819581000..004cf009fc 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() { @@ -1185,6 +1202,28 @@ namespace Avalonia.Controls.UnitTests.Primitives target.MoveSelection(NavigationDirection.Next, true); } + [Fact] + public void MoveSelection_Does_Select_Disabled_Controls() + { + // Issue #3426. + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem(), + new ListBoxItem { IsEnabled = false }, + }, + SelectedIndex = 0, + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Next, true); + + Assert.Equal(0, target.SelectedIndex); + } + [Fact] public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_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.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 2238175a4a..f281c83bb3 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -65,6 +65,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(10, 10), target.Offset); } + [Fact] + public void Test_ScrollToHome() + { + var target = new ScrollViewer(); + target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); + target.Offset = new Vector(25, 25); + target.ScrollToHome(); + + Assert.Equal(new Vector(0, 0), target.Offset); + } + + [Fact] + public void Test_ScrollToEnd() + { + var target = new ScrollViewer(); + target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10)); + target.Offset = new Vector(25, 25); + target.ScrollToEnd(); + + Assert.Equal(new Vector(0, 40), target.Offset); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 72c81659c3..91592b1ff7 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -166,6 +166,31 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Impl_Close_Should_Raise_DetachedFromLogicalTree_Event() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + + var target = new TestTopLevel(impl.Object); + var raised = 0; + + target.DetachedFromLogicalTree += (s, e) => + { + Assert.Same(target, e.Root); + Assert.Same(target, e.Source); + Assert.Null(e.Parent); + ++raised; + }; + + impl.Object.Closed(); + + Assert.Equal(1, raised); + } + } + [Fact] public void Impl_Input_Should_Pass_Input_To_InputManager() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index ed8a39d063..a2892be5c2 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -971,6 +971,40 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Removing_TreeView_From_Root_Should_Preserve_TreeViewItems() + { + // Issue #3328 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var root = new TestRoot(); + root.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + + root.Child = null; + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + + var rootNode = Assert.IsType(target.Presenter.Panel.Children[0]); + Assert.Equal(3, rootNode.ItemContainerGenerator.Containers.Count()); + Assert.Equal(3, rootNode.Presenter.Panel.Children.Count); + + var child2Node = Assert.IsType(rootNode.Presenter.Panel.Children[1]); + Assert.Equal(1, child2Node.ItemContainerGenerator.Containers.Count()); + Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 0508edd92f..81699b075c 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -341,11 +341,62 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.Manual, + Content = child + }; + + target.Show(); + + Assert.Equal(new Size(100, 50), child.MeasureSize); + } + } + + [Fact] + public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(Size.Infinity, child.MeasureSize); + } + } + private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => x.Scaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } + + private class ChildControl : Control + { + public Size MeasureSize { get; private set; } + + protected override Size MeasureOverride(Size availableSize) + { + MeasureSize = availableSize; + return base.MeasureOverride(availableSize); + } + } } } 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/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs index 39c219a773..ffa03f9e8e 100644 --- a/tests/Avalonia.Input.UnitTests/GesturesTests.cs +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -135,7 +135,7 @@ namespace Avalonia.Interactivity.UnitTests } [Fact] - public void DoubleTapped_Should_Be_Raised_For_Middle_Button() + public void DoubleTapped_Should_Not_Be_Raised_For_Middle_Button() { Border border = new Border(); var decorator = new Decorator @@ -149,7 +149,7 @@ namespace Avalonia.Interactivity.UnitTests _mouse.Click(border, MouseButton.Middle); _mouse.Down(border, MouseButton.Middle, clickCount: 2); - Assert.True(raised); + Assert.False(raised); } [Fact] 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.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 3f20c9a76a..0165b91844 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -374,5 +374,38 @@ namespace Avalonia.Layout.UnitTests Assert.True(control.Measured); Assert.True(control.IsMeasureValid); } + + [Fact] + public void Calling_ExecuteLayoutPass_From_ExecuteInitialLayoutPass_Does_Not_Break_Measure() + { + // Test for issue #3550. + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + var count = 0; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Measured = false; + + control.DoMeasureOverride = (l, s) => + { + if (count++ == 0) + { + control.InvalidateMeasure(); + root.LayoutManager.ExecuteLayoutPass(); + return new Size(100, 100); + } + else + { + return new Size(200, 200); + } + }; + + root.InvalidateMeasure(); + control.InvalidateMeasure(); + root.LayoutManager.ExecuteInitialLayoutPass(root); + + Assert.Equal(new Size(200, 200), control.Bounds.Size); + Assert.Equal(new Size(200, 200), control.DesiredSize); + } } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 1da4746516..3b288dbf66 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -8,8 +8,10 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Diagnostics; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using JetBrains.dotMemoryUnit; @@ -370,6 +372,56 @@ namespace Avalonia.LeakTests } } + [Fact] + public void Control_With_Style_RenderTransform_Is_Freed() + { + // # Issue #3545 + using (Start()) + { + Func run = () => + { + var window = new Window + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Visual.RenderTransformProperty, + Value = new RotateTransform(45), + } + } + } + }, + Content = new Canvas() + }; + + window.Show(); + + // Do a layout and make sure that Canvas gets added to visual tree with + // its render transform. + window.LayoutManager.ExecuteInitialLayoutPass(window); + var canvas = Assert.IsType(window.Presenter.Child); + Assert.IsType(canvas.RenderTransform); + + // Clear the content and ensure the Canvas is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow); @@ -397,6 +449,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/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 + +