diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 3923bdeeda..d61268173d 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -4,7 +4,6 @@ - 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/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index e7abedae51..ed00f8c5e1 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -157,11 +157,14 @@ NSArray* _modes; -(void) perform { + ComPtr cb; @synchronized (self) { _signaled = false; - if(_parent != NULL && _parent->SignaledCallback != NULL) - _parent->SignaledCallback->Signaled(0, false); + if(_parent != NULL) + cb = _parent->SignaledCallback; } + if(cb != nullptr) + cb->Signaled(0, false); } -(void) setParent:(PlatformThreadingInterface *)parent 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 e39a061e99..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; @@ -823,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(); } } @@ -928,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]; @@ -1478,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]; @@ -1502,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/readme.md b/readme.md index 783857a096..42b1e52205 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ **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 | |---|---|---| @@ -24,6 +24,8 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +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/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 3005fdbc51..a3f16dc222 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net461 + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 4c2769bcc4..11c0a4fa68 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -21,6 +21,7 @@ + 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/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 3005fdbc51..a3f16dc222 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net461 + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 3005fdbc51..a3f16dc222 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net461 + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6a00feaf79..b0ff591682 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -479,7 +478,13 @@ namespace Avalonia } } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) + { + ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); + } /// /// Called for each inherited property when the changes. diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index b305a9aaa2..aa7a675764 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -20,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; @@ -53,7 +52,6 @@ namespace Avalonia throw new ArgumentException("'name' may not contain periods."); } - _initialized = new Subject(); _changed = new Subject(); _metadata = new Dictionary(); @@ -81,7 +79,6 @@ namespace Avalonia Contract.Requires(source != null); Contract.Requires(ownerType != null); - _initialized = source._initialized; _changed = source._changed; _metadata = new Dictionary(); @@ -136,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. @@ -488,26 +469,6 @@ namespace Avalonia return Name; } - /// - /// True if has any observers. - /// - internal bool HasNotifyInitializedObservers => _initialized.HasObservers; - - /// - /// Notifies the observable. - /// - /// The object being initialized. - internal abstract void NotifyInitialized(IAvaloniaObject o); - - /// - /// Notifies the observable. - /// - /// The observable arguments. - internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e) - { - _initialized.OnNext(e); - } - /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 14c8630599..29ab10278b 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -415,51 +415,6 @@ namespace Avalonia _inheritedCache.Clear(); } - internal void NotifyInitialized(AvaloniaObject o) - { - Contract.Requires(o != null); - - var type = o.GetType(); - - 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) - { - data.Property.NotifyInitialized(o); - } - } - private readonly struct PropertyInitializationData { public AvaloniaProperty Property { get; } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 7a0be065eb..39ed3b084f 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -101,21 +101,6 @@ namespace Avalonia return (DirectPropertyMetadata)base.GetMetadata(type); } - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - InvokeGetter(o), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 09a0f169df..3249b31d66 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this); + _sink.Completed(Property, this, Value); } - public void OnCompleted() => _sink.Completed(Property, this); + public void OnCompleted() => _sink.Completed(Property, this, Value); public void OnError(Exception error) { diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index 223b0058c1..9012a985ac 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore Optional oldValue, BindingValue newValue); - void Completed(AvaloniaProperty property, IPriorityValueEntry entry); + void Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue); } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 2785dc6840..4ef8f650fa 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -117,7 +117,10 @@ namespace Avalonia.PropertyStore UpdateEffectiveValue(); } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); UpdateEffectiveValue(); diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8c4d683ae0..d1f961a567 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -181,21 +181,6 @@ namespace Avalonia /// object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - /// - internal override void NotifyInitialized(IAvaloniaObject o) - { - if (HasNotifyInitializedObservers) - { - var e = new AvaloniaPropertyChangedEventArgs( - o, - this, - default, - o.GetValue(this), - BindingPriority.Unset); - NotifyInitialized(e); - } - } - /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index d1393a9c0d..8a25d29c3f 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.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.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -185,6 +186,14 @@ namespace Avalonia.Utilities } } + var typeConverter = TypeDescriptor.GetConverter(to); + + if (typeConverter.CanConvertFrom(from) == true) + { + result = typeConverter.ConvertFrom(null, culture, value); + return true; + } + var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) 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 58ebc48652..e310be0f0a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -148,7 +148,7 @@ namespace Avalonia _values.Remove(property); _sink.ValueChanged( property, - BindingPriority.LocalValue, + BindingPriority.Unset, old, BindingValue.Unset); } @@ -173,7 +173,7 @@ namespace Avalonia { return new Diagnostics.AvaloniaPropertyValue( property, - slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, + slot.Value.HasValue ? slot.Value.Value : AvaloniaProperty.UnsetValue, slot.ValuePriority, null); } @@ -190,13 +190,17 @@ namespace Avalonia _sink.ValueChanged(property, priority, oldValue, newValue); } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { if (_values.TryGetValue(property, out var slot)) { if (slot == entry) { _values.Remove(property); + _sink.Completed(property, entry, oldValue); } } } @@ -228,6 +232,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); + priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index e16e11cdd6..844316741a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -65,7 +65,7 @@ namespace Avalonia.Controls private const double DATAGRID_minimumColumnHeaderHeight = 4; internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 48.0; + private const double DATAGRID_mouseWheelDelta = 72.0; private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_defaultRowHeight = 22; @@ -149,6 +149,9 @@ namespace Avalonia.Controls private IEnumerable _items; + public event EventHandler HorizontalScroll; + public event EventHandler VerticalScroll; + /// /// Identifies the CanUserReorderColumns dependency property. /// @@ -373,7 +376,11 @@ 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 = @@ -656,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)); @@ -4223,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) @@ -5555,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 diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 062fdeb4d3..31c77c595d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls static DataGridColumnHeader() { - AreSeparatorsVisibleProperty.Changed.AddClassHandler((x,e) => x.OnAreSeparatorsVisibleChanged(e)); + AreSeparatorsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreSeparatorsVisibleChanged(e)); } /// @@ -103,7 +103,7 @@ namespace Avalonia.Controls { get; set; - } + } internal DataGrid OwningGrid => OwningColumn?.OwningGrid; internal int ColumnIndex @@ -116,19 +116,19 @@ namespace Avalonia.Controls } return OwningColumn.Index; } - } + } internal ListSortDirection? CurrentSortingState { get; private set; - } + } private bool IsMouseOver { get; set; - } + } private bool IsPressed { @@ -158,14 +158,14 @@ namespace Avalonia.Controls && OwningGrid.DataConnection.AllowSort) { var sort = OwningColumn.GetSortDescription(); - if(sort != null) + if (sort != null) { CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; } } - PseudoClasses.Set(":sortascending", + PseudoClasses.Set(":sortascending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); - PseudoClasses.Set(":sortdescending", + PseudoClasses.Set(":sortdescending", CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); } @@ -195,7 +195,7 @@ namespace Avalonia.Controls // completed a click without dragging, so we're sorting InvokeProcessSort(keyModifiers); handled = true; - } + } internal void InvokeProcessSort(KeyModifiers keyModifiers) { @@ -208,7 +208,7 @@ namespace Avalonia.Controls { Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); } - } + } //TODO GroupSorting internal void ProcessSort(KeyModifiers keyModifiers) @@ -246,45 +246,49 @@ namespace Avalonia.Controls owningGrid.DataConnection.SortDescriptions.Clear(); } - if (sort != null) + // if ctrl is held down, we only clear the sort directions + if (!ctrl) { - newSort = sort.SwitchSortDirection(); - - // changing direction should not affect sort order, so we replace this column's - // sort description instead of just adding it to the end of the collection - int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); - if (oldIndex >= 0) + if (sort != null) { - owningGrid.DataConnection.SortDescriptions.Remove(sort); - owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + newSort = sort.SwitchSortDirection(); + + // changing direction should not affect sort order, so we replace this column's + // sort description instead of just adding it to the end of the collection + int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); + if (oldIndex >= 0) + { + owningGrid.DataConnection.SortDescriptions.Remove(sort); + owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + } + else + { + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } } else { + string propertyName = OwningColumn.GetSortPropertyName(); + // no-opt if we couldn't find a property to sort on + if (string.IsNullOrEmpty(propertyName)) + { + return; + } + + newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); owningGrid.DataConnection.SortDescriptions.Add(newSort); } } - else - { - string propertyName = OwningColumn.GetSortPropertyName(); - // no-opt if we couldn't find a property to sort on - if (string.IsNullOrEmpty(propertyName)) - { - return; - } - - newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); - owningGrid.DataConnection.SortDescriptions.Add(newSort); - } } } - } + } private bool CanReorderColumn(DataGridColumn column) { - return OwningGrid.CanUserReorderColumns + return OwningGrid.CanUserReorderColumns && !(column is DataGridFillerColumn) && (column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value || !column.CanUserReorderInternal.HasValue); - } + } /// /// Determines whether a column can be resized by dragging the border of its header. If star sizing @@ -302,7 +306,7 @@ namespace Avalonia.Controls return false; } return column.ActualCanUserResize; - } + } private static bool TrySetResizeColumn(DataGridColumn column) { @@ -316,7 +320,7 @@ namespace Avalonia.Controls return true; } return false; - } + } //TODO DragDrop @@ -326,7 +330,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 +375,7 @@ namespace Avalonia.Controls { if (_dragMode == DragMode.MouseDown) { - OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled); + OnMouseLeftButtonUp_Click(args.KeyModifiers, ref handled); } else if (_dragMode == DragMode.Reorder) { @@ -391,7 +395,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; @@ -449,7 +453,7 @@ namespace Avalonia.Controls OnMouseLeave(); ApplyState(); - } + } private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e) { @@ -577,7 +581,7 @@ namespace Avalonia.Controls { return OwningGrid.Columns.Count - 1; } - } + } /// /// Returns true if the mouse is @@ -723,7 +727,7 @@ namespace Avalonia.Controls Point targetPosition = new Point(0, 0); if (targetColumn == null || targetColumn == OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != OwningColumn.IsFrozen) { - targetColumn = + targetColumn = OwningGrid.ColumnsInternal.GetLastColumn( isVisible: true, isFrozen: OwningColumn.IsFrozen, @@ -741,7 +745,7 @@ namespace Avalonia.Controls handled = true; } - } + } private void OnMouseMove_Resize(ref bool handled, Point mousePositionHeaders) { @@ -764,7 +768,7 @@ namespace Avalonia.Controls handled = true; } - } + } private void SetDragCursor(Point mousePosition) { diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 7a399349f6..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.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs new file mode 100644 index 0000000000..ae70b59fde --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class ConsoleView : UserControl + { + private readonly ListBox _historyList; + private readonly TextBox _input; + + public ConsoleView() + { + this.InitializeComponent(); + _historyList = this.FindControl("historyList"); + ((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged; + _input = this.FindControl("input"); + _input.KeyDown += InputKeyDown; + } + + public void FocusInput() => _input.Focus(); + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) + { + DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); + } + } + + private void InputKeyDown(object sender, KeyEventArgs e) + { + var vm = (ConsoleViewModel)DataContext; + + switch (e.Key) + { + case Key.Enter: + vm.Execute(); + e.Handled = true; + break; + case Key.Up: + vm.HistoryUp(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + case Key.Down: + vm.HistoryDown(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml new file mode 100644 index 0000000000..29ce26fcdf --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs new file mode 100644 index 0000000000..c6bd5a18aa --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Diagnostics.Views +{ + internal class ControlDetailsView : UserControl + { + public ControlDetailsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml similarity index 92% rename from src/Avalonia.Diagnostics/Views/EventsView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml index 406dd433a2..b7f0860e70 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml @@ -1,9 +1,10 @@  + xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" + x:Class="Avalonia.Diagnostics.Views.EventsPageView"> - + ("events"); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml new file mode 100644 index 0000000000..663722acba --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs new file mode 100644 index 0000000000..783709e54b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainView : UserControl + { + private readonly ConsoleView _console; + private readonly GridSplitter _consoleSplitter; + private readonly Grid _rootGrid; + private readonly int _consoleRow; + private double _consoleHeight = -1; + + public MainView() + { + InitializeComponent(); + AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); + _console = this.FindControl("console"); + _consoleSplitter = this.FindControl("consoleSplitter"); + _rootGrid = this.FindControl("rootGrid"); + _consoleRow = Grid.GetRow(_console); + } + + public void ToggleConsole() + { + var vm = (MainViewModel)DataContext; + + if (_consoleHeight == -1) + { + _consoleHeight = Bounds.Height / 3; + } + + vm.Console.ToggleVisibility(); + _consoleSplitter.IsVisible = vm.Console.IsVisible; + + if (vm.Console.IsVisible) + { + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(_consoleHeight, GridUnitType.Pixel); + Dispatcher.UIThread.Post(() => _console.FocusInput(), DispatcherPriority.Background); + } + else + { + _consoleHeight = _rootGrid.RowDefinitions[_consoleRow].Height.Value; + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(0, GridUnitType.Pixel); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + ToggleConsole(); + e.Handled = true; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml new file mode 100644 index 0000000000..3623e95597 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs new file mode 100644 index 0000000000..3abdb5034a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainWindow : Window, IStyleHost + { + private TopLevel _root; + private IDisposable _keySubscription; + + public MainWindow() + { + InitializeComponent(); + + _keySubscription = InputManager.Instance.Process + .OfType() + .Subscribe(RawKeyDown); + } + + public TopLevel Root + { + get => _root; + set + { + if (_root != value) + { + _root = value; + DataContext = new MainViewModel(value); + } + } + } + + IStyleHost IStyleHost.StylingParent => null; + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _keySubscription.Dispose(); + ((MainViewModel)DataContext)?.Dispose(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void RawKeyDown(RawKeyEventArgs e) + { + const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; + + if (e.Modifiers == modifiers) + { + var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; + var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) + .FirstOrDefault(); + + if (control != null) + { + var vm = (MainViewModel)DataContext; + vm.SelectControl((IControl)control); + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml similarity index 60% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 2619fd744a..a1e6ca7d37 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -1,26 +1,29 @@ - - + + - - + + - - + + diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs similarity index 90% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1326f718de..633d18ddd8 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,6 +1,3 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; @@ -12,14 +9,14 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.Views { - public class TreePageView : UserControl + internal class TreePageView : UserControl { private Control _adorner; private TreeView _tree; public TreePageView() { - InitializeComponent(); + this.InitializeComponent(); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; } @@ -39,7 +36,7 @@ namespace Avalonia.Diagnostics.Views _adorner = new Rectangle { Fill = new SolidColorBrush(0x80a0c5e8), - [AdornerLayer.AdornedElementProperty] = node.Visual + [AdornerLayer.AdornedElementProperty] = node.Visual, }; layer.Children.Add(_adorner); diff --git a/src/Avalonia.Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs similarity index 100% rename from src/Avalonia.Diagnostics/VisualTreeDebug.cs rename to src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs deleted file mode 100644 index 4b832f7ce6..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class ControlDetailsViewModel : ViewModelBase - { - public ControlDetailsViewModel(IVisual control) - { - if (control is AvaloniaObject avaloniaObject) - { - Properties = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject) - .Select(x => new PropertyDetails(avaloniaObject, x)) - .OrderBy(x => x.IsAttached) - .ThenBy(x => x.Name); - } - } - - public IEnumerable Properties { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs deleted file mode 100644 index 9f524a21eb..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ /dev/null @@ -1,76 +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.ObjectModel; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Input; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class DevToolsViewModel : ViewModelBase - { - private IDevToolViewModel _selectedTool; - private string _focusedControl; - private string _pointerOverElement; - - public DevToolsViewModel(IControl root) - { - Tools = new ObservableCollection - { - new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), - new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), - new EventsViewModel(root) - }; - - SelectedTool = Tools.First(); - - UpdateFocusedControl(); - - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - - root.GetObservable(TopLevel.PointerOverElementProperty) - .Subscribe(x => PointerOverElement = x?.GetType().Name); - } - - public IDevToolViewModel SelectedTool - { - get => _selectedTool; - set => RaiseAndSetIfChanged(ref _selectedTool, value); - } - - public ObservableCollection Tools { get; } - - public string FocusedControl - { - get => _focusedControl; - private set => RaiseAndSetIfChanged(ref _focusedControl, value); - } - - public string PointerOverElement - { - get => _pointerOverElement; - private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); - } - - public void SelectControl(IControl control) - { - if (SelectedTool is TreePageViewModel tree) - { - tree.SelectControl(control); - } - } - - private void UpdateFocusedControl() - { - FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; - } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs deleted file mode 100644 index 0434230a63..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs +++ /dev/null @@ -1,16 +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.Diagnostics.ViewModels -{ - /// - /// View model interface for tool showing up in DevTools - /// - public interface IDevToolViewModel - { - /// - /// Name of a tool. - /// - string Name { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs deleted file mode 100644 index 523be406c8..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ /dev/null @@ -1,58 +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; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class PropertyDetails : ViewModelBase - { - private object _value; - private string _priority; - private string _diagnostic; - - public PropertyDetails(AvaloniaObject o, AvaloniaProperty property) - { - Name = property.IsAttached ? - $"[{property.OwnerType.Name}.{property.Name}]" : - property.Name; - IsAttached = property.IsAttached; - - // TODO: Unsubscribe when view model is deactivated. - o.GetObservable(property).Subscribe(x => - { - var diagnostic = o.GetDiagnostic(property); - Value = diagnostic.Value ?? "(null)"; - Priority = (diagnostic.Priority != BindingPriority.Unset) ? - diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? - "Inherited" : - "Unset"; - Diagnostic = diagnostic.Diagnostic; - }); - } - - public string Name { get; } - - public bool IsAttached { get; } - - public string Priority - { - get => _priority; - private set => RaiseAndSetIfChanged(ref _priority, value); - } - - public string Diagnostic - { - get => _diagnostic; - private set => RaiseAndSetIfChanged(ref _diagnostic, value); - } - - public object Value - { - get => _value; - private set => RaiseAndSetIfChanged(ref _value, value); - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs deleted file mode 100644 index fb867ab55e..0000000000 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ /dev/null @@ -1,75 +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.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Diagnostics.ViewModels; -using Avalonia.Media; - -namespace Avalonia.Diagnostics.Views -{ - internal class ControlDetailsView : UserControl - { - private static readonly StyledProperty ViewModelProperty = - AvaloniaProperty.Register(nameof(ViewModel)); - - private SimpleGrid _grid; - - public ControlDetailsView() - { - InitializeComponent(); - this.GetObservable(DataContextProperty) - .Subscribe(x => ViewModel = (ControlDetailsViewModel)x); - } - - public ControlDetailsViewModel ViewModel - { - get => GetValue(ViewModelProperty); - private set - { - SetValue(ViewModelProperty, value); - _grid[GridRepeater.ItemsProperty] = value?.Properties; - } - } - - private void InitializeComponent() - { - Func> pt = PropertyTemplate; - - Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; - } - - private IEnumerable PropertyTemplate(object i) - { - var property = (PropertyDetails)i; - - var margin = new Thickness(2); - - yield return new TextBlock - { - Margin = margin, - Text = property.Name, - TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) - .Select(v => v?.ToString()) - .ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() - }; - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/GridRepeater.cs b/src/Avalonia.Diagnostics/Views/GridRepeater.cs deleted file mode 100644 index b0ff26c7b6..0000000000 --- a/src/Avalonia.Diagnostics/Views/GridRepeater.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 System.Collections; -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - internal static class GridRepeater - { - public static readonly AttachedProperty ItemsProperty = - AvaloniaProperty.RegisterAttached("Items", typeof(GridRepeater)); - - public static readonly AttachedProperty>> TemplateProperty = - AvaloniaProperty.RegisterAttached>>("Template", - typeof(GridRepeater)); - - static GridRepeater() - { - ItemsProperty.Changed.Subscribe(ItemsChanged); - } - - private static void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - var grid = (SimpleGrid)e.Sender; - var items = (IEnumerable)e.NewValue; - var template = grid.GetValue(TemplateProperty); - - grid.Children.Clear(); - - if (items != null) - { - int count = 0; - int cols = 3; - - foreach (var item in items) - { - foreach (var control in template(item)) - { - grid.Children.Add(control); - SimpleGrid.SetColumn(control, count % cols); - SimpleGrid.SetRow(control, count / cols); - ++count; - } - } - } - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs deleted file mode 100644 index 24b2f29463..0000000000 --- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs +++ /dev/null @@ -1,33 +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.ComponentModel; -using System.Reactive.Linq; -using System.Reflection; - -namespace Avalonia.Diagnostics.Views -{ - internal static class PropertyChangedExtensions - { - public static IObservable GetObservable(this INotifyPropertyChanged source, string propertyName) - { - Contract.Requires(source != null); - Contract.Requires(propertyName != null); - - var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); - - if (property == null) - { - throw new ArgumentException($"Property '{propertyName}' not found on '{source}."); - } - - return Observable.FromEventPattern( - e => source.PropertyChanged += e, - e => source.PropertyChanged -= e) - .Where(e => e.EventArgs.PropertyName == propertyName) - .Select(_ => (T)property.GetValue(source)) - .StartWith((T)property.GetValue(source)); - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs deleted file mode 100644 index 4fc77666e1..0000000000 --- a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - /// - /// A simple grid control that lays out columns with a equal width and rows to their desired - /// size. - /// - /// - /// This is used in the devtools because our performance sucks. - /// - public class SimpleGrid : Panel - { - private readonly List _columnWidths = new List(); - private readonly List _rowHeights = new List(); - private double _totalWidth; - private double _totalHeight; - - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached("Column"); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached("Row"); - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(IControl control) - { - return control.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(IControl control) - { - return control.GetValue(RowProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(IControl control, int value) - { - control.SetValue(ColumnProperty, value); - } - - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(IControl control, int value) - { - control.SetValue(RowProperty, value); - } - - protected override Size MeasureOverride(Size availableSize) - { - _columnWidths.Clear(); - _rowHeights.Clear(); - _totalWidth = 0; - _totalHeight = 0; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - - child.Measure(availableSize); - - var desired = child.DesiredSize; - UpdateCell(_columnWidths, column, desired.Width, ref _totalWidth); - UpdateCell(_rowHeights, row, desired.Height, ref _totalHeight); - } - - return new Size(_totalWidth, _totalHeight); - } - - protected override Size ArrangeOverride(Size finalSize) - { - var columnWidth = finalSize.Width / _columnWidths.Count; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - var rect = new Rect(column * columnWidth, GetRowTop(row), columnWidth, _rowHeights[row]); - child.Arrange(rect); - } - - return new Size(finalSize.Width, _totalHeight); - } - - private double UpdateCell(IList cells, int cell, double value, ref double total) - { - while (cells.Count < cell + 1) - { - cells.Add(0); - } - - var existing = cells[cell]; - - if (value > existing) - { - cells[cell] = value; - total += value - existing; - return value; - } - else - { - return existing; - } - } - - private double GetRowTop(int row) - { - var result = 0.0; - - for (var i = 0; i < row; ++i) - { - result += _rowHeights[i]; - } - - return result; - } - } -} diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index e227fc2812..e51cc2f3ce 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -99,7 +99,7 @@ - + \ No newline at end of file diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index cb86e271a6..f54ddf32be 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs index 91b224e65a..112abb1a4e 100644 --- a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -111,9 +111,7 @@ namespace Avalonia.Input.GestureRecognizers _pointerGrabs.Remove(e.Pointer); foreach (var r in _recognizers) { - if(e.Handled) - break; - r.PointerCaptureLost(e); + r.PointerCaptureLost(e.Pointer); } } @@ -121,6 +119,11 @@ namespace Avalonia.Input.GestureRecognizers { pointer.Capture(_inputElement); _pointerGrabs[pointer] = recognizer; + foreach (var r in _recognizers) + { + if (r != recognizer) + r.PointerCaptureLost(pointer); + } } } diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs index b8ba9e529c..c1d9ae5304 100644 --- a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs @@ -6,7 +6,7 @@ namespace Avalonia.Input.GestureRecognizers void PointerPressed(PointerPressedEventArgs e); void PointerReleased(PointerReleasedEventArgs e); void PointerMoved(PointerEventArgs e); - void PointerCaptureLost(PointerCaptureLostEventArgs e); + void PointerCaptureLost(IPointer pointer); } public interface IGestureRecognizerActionsDispatcher diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 4f3c7c0bba..e022401c8e 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -116,9 +116,9 @@ namespace Avalonia.Input.GestureRecognizers } } - public void PointerCaptureLost(PointerCaptureLostEventArgs e) + public void PointerCaptureLost(IPointer pointer) { - if (e.Pointer == _tracking) EndGesture(); + if (pointer == _tracking) EndGesture(); } void EndGesture() @@ -148,6 +148,7 @@ namespace Avalonia.Input.GestureRecognizers EndGesture(); else { + _tracking = null; var savedGestureId = _gestureId; var st = Stopwatch.StartNew(); var lastTime = TimeSpan.Zero; diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 9247fb48a9..aa5e0dd8ec 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -63,7 +63,7 @@ namespace Avalonia.Input event EventHandler PointerReleased; /// - /// Occurs when the mouse wheen is scrolled over the control. + /// Occurs when the mouse wheel is scrolled over the control. /// event EventHandler PointerWheelChanged; diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 1e2a621bd1..121412f2e4 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -181,10 +183,11 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + } - PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); - PseudoClass(IsFocusedProperty, ":focus"); - PseudoClass(IsPointerOverProperty, ":pointerover"); + public InputElement() + { + UpdatePseudoClasses(IsFocused, IsPointerOver); } /// @@ -372,7 +375,11 @@ namespace Avalonia.Input public bool IsEffectivelyEnabled { get => _isEffectivelyEnabled; - private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + private set + { + SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + PseudoClasses.Set(":disabled", !value); + } } public List KeyBindings { get; } = new List(); @@ -522,6 +529,20 @@ namespace Avalonia.Input { } + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + { + base.OnPropertyChanged(property, oldValue, newValue, priority); + + if (property == IsFocusedProperty) + { + UpdatePseudoClasses(newValue.GetValueOrDefault(), null); + } + else if (property == IsPointerOverProperty) + { + UpdatePseudoClasses(null, newValue.GetValueOrDefault()); + } + } + /// /// Updates the property value according to the parent /// control's enabled state and . @@ -578,5 +599,18 @@ namespace Avalonia.Input child?.UpdateIsEffectivelyEnabled(this); } } + + private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) + { + if (isFocused.HasValue) + { + PseudoClasses.Set(":focus", isFocused.Value); + } + + if (isPointerOver.HasValue) + { + PseudoClasses.Set(":pointerover", isPointerOver.Value); + } + } } } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index 406890b767..0e7e991260 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -100,12 +100,12 @@ namespace Avalonia.Input /// public static NavigationDirection? ToNavigationDirection( this Key key, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers modifiers = KeyModifiers.None) { switch (key) { case Key.Tab: - return (modifiers & InputModifiers.Shift) != 0 ? + return (modifiers & KeyModifiers.Shift) != 0 ? NavigationDirection.Next : NavigationDirection.Previous; case Key.Up: return NavigationDirection.Up; diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 1792655a13..69a4406828 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -132,8 +132,16 @@ namespace Avalonia.Layout /// public void ExecuteInitialLayoutPass(ILayoutRoot root) { - Measure(root); - Arrange(root); + try + { + _running = true; + Measure(root); + Arrange(root); + } + finally + { + _running = false; + } // Running the initial layout pass may have caused some control to be invalidated // so run a full layout pass now (this usually due to scrollbars; its not known diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs deleted file mode 100644 index ce2b03e355..0000000000 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ /dev/null @@ -1,42 +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.Threading; -using Avalonia.Native.Interop; -using Avalonia.Rendering; - -namespace Avalonia.Native -{ - public class AvaloniaNativeDeferredRendererLock : IDeferredRendererLock - { - private readonly IAvnWindowBase _window; - - public AvaloniaNativeDeferredRendererLock(IAvnWindowBase window) - { - _window = window; - } - - public IDisposable TryLock() - { - if (_window.TryLock()) - return new UnlockDisposable(_window); - return null; - } - - private sealed class UnlockDisposable : IDisposable - { - private IAvnWindowBase _window; - - public UnlockDisposable(IAvnWindowBase window) - { - _window = window; - } - - public void Dispose() - { - Interlocked.Exchange(ref _window, null)?.Unlock(); - } - } - } -} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 756619fa9f..7fc067ee4e 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Runtime.InteropServices; +using System.Security.Cryptography; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; @@ -17,6 +18,7 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; + private GlPlatformFeature _glFeature; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); @@ -87,7 +89,7 @@ namespace Avalonia.Native _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } - + AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) @@ -100,14 +102,16 @@ namespace Avalonia.Native .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) - .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); + if (_options.UseGpu) + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); } public IWindowImpl CreateWindow() { - return new WindowImpl(_factory, _options); + return new WindowImpl(_factory, _options, _glFeature); } public IEmbeddableWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Native/DeferredFramebuffer.cs b/src/Avalonia.Native/DeferredFramebuffer.cs index adc721de42..0c32bce145 100644 --- a/src/Avalonia.Native/DeferredFramebuffer.cs +++ b/src/Avalonia.Native/DeferredFramebuffer.cs @@ -21,7 +21,7 @@ namespace Avalonia.Native Size = new PixelSize(width, height); RowBytes = width * 4; Dpi = dpi; - Format = PixelFormat.Rgba8888; + Format = PixelFormat.Bgra8888; } public IntPtr Address { get; set; } diff --git a/src/Avalonia.Native/GlPlatformFeature.cs b/src/Avalonia.Native/GlPlatformFeature.cs index e39680bdee..8e3448795c 100644 --- a/src/Avalonia.Native/GlPlatformFeature.cs +++ b/src/Avalonia.Native/GlPlatformFeature.cs @@ -8,24 +8,31 @@ namespace Avalonia.Native { class GlPlatformFeature : IWindowingPlatformGlFeature { - - public GlPlatformFeature(IAvnGlFeature feature) + public GlPlatformFeature(IAvnGlDisplay display) { - Display = new GlDisplay(feature.ObtainDisplay()); - ImmediateContext = new GlContext(Display, feature.ObtainImmediateContext()); + var immediate = display.CreateContext(null); + var deferred = display.CreateContext(immediate); + GlDisplay = new GlDisplay(display, immediate.SampleCount, immediate.StencilSize); + + ImmediateContext = new GlContext(Display, immediate); + DeferredContext = new GlContext(Display, deferred); } public IGlContext ImmediateContext { get; } - public GlDisplay Display { get; } + internal GlContext DeferredContext { get; } + internal GlDisplay GlDisplay; + public GlDisplay Display => GlDisplay; } class GlDisplay : IGlDisplay { private readonly IAvnGlDisplay _display; - public GlDisplay(IAvnGlDisplay display) + public GlDisplay(IAvnGlDisplay display, int sampleCount, int stencilSize) { _display = display; + SampleCount = sampleCount; + StencilSize = stencilSize; GlInterface = new GlInterface((name, optional) => { var rv = _display.GetProcAddress(name); @@ -39,11 +46,11 @@ namespace Avalonia.Native public GlInterface GlInterface { get; } - public int SampleCount => _display.GetSampleCount(); + public int SampleCount { get; } - public int StencilSize => _display.GetStencilSize(); + public int StencilSize { get; } - public void ClearContext() => _display.ClearContext(); + public void ClearContext() => _display.LegacyClearCurrentContext(); } class GlContext : IGlContext @@ -60,7 +67,7 @@ namespace Avalonia.Native public void MakeCurrent() { - Context.MakeCurrent(); + Context.LegacyMakeCurrent(); } } @@ -109,6 +116,9 @@ namespace Avalonia.Native public double Scaling => _session.GetScaling(); + + public bool IsYFlipped => true; + public void Dispose() { _session?.Dispose(); @@ -128,5 +138,6 @@ namespace Avalonia.Native { return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget()); } + } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index d7fa1052ff..9423b2aa2a 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -8,19 +8,23 @@ using Avalonia.Platform; namespace Avalonia.Native { - public class PopupImpl : WindowBaseImpl, IPopupImpl + class PopupImpl : WindowBaseImpl, IPopupImpl { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; + private readonly GlPlatformFeature _glFeature; + public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - IWindowBaseImpl parent) : base(opts) + GlPlatformFeature glFeature, + IWindowBaseImpl parent) : base(opts, glFeature) { _factory = factory; _opts = opts; + _glFeature = glFeature; using (var e = new PopupEvents(this)) { - Init(factory.CreatePopup(e), factory.CreateScreens()); + Init(factory.CreatePopup(e, _opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens()); } PopupPositioner = new ManagedPopupPositioner(new OsxManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } @@ -51,7 +55,7 @@ namespace Avalonia.Native } } - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index a7828bedaf..c757576017 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,14 +14,18 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; + private readonly GlPlatformFeature _glFeature; IAvnWindow _native; - public WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts) : base(opts) + internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, + GlPlatformFeature glFeature) : base(opts, glFeature) { _factory = factory; _opts = opts; + _glFeature = glFeature; using (var e = new WindowEvents(this)) { - Init(_native = factory.CreateWindow(e), factory.CreateScreens()); + Init(_native = factory.CreateWindow(e, + _opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens()); } NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); @@ -113,6 +117,6 @@ namespace Avalonia.Native public void Move(PixelPoint point) => Position = point; public override IPopupImpl CreatePopup() => - _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this); + _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this); } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 5d701dc8df..331f083e8b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -60,9 +60,9 @@ namespace Avalonia.Native private double _savedScaling; private GlPlatformSurface _glSurface; - public WindowBaseImpl(AvaloniaNativePlatformOptions opts) + internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) { - _gpu = opts.UseGpu; + _gpu = opts.UseGpu && glFeature != null; _deferredRendering = opts.UseDeferredRendering; _keyboard = AvaloniaLocator.Current.GetService(); @@ -75,8 +75,8 @@ namespace Avalonia.Native _native = window; Handle = new MacOSTopLevelWindowHandle(window); - - _glSurface = new GlPlatformSurface(window); + if (_gpu) + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; _savedScaling = Scaling; @@ -103,25 +103,20 @@ namespace Avalonia.Native public ILockedFramebuffer Lock() { - if(_deferredRendering) + var w = _savedLogicalSize.Width * _savedScaling; + var h = _savedLogicalSize.Height * _savedScaling; + var dpi = _savedScaling * 96; + return new DeferredFramebuffer(cb => { - var w = _savedLogicalSize.Width * _savedScaling; - var h = _savedLogicalSize.Height * _savedScaling; - var dpi = _savedScaling * 96; - return new DeferredFramebuffer(cb => + lock (_syncRoot) { - lock (_syncRoot) - { - if (_native == null) - return false; - cb(_native); - _lastRenderedLogicalSize = _savedLogicalSize; - return true; - } - }, (int)w, (int)h, new Vector(dpi, dpi)); - } - - return new FramebufferWrapper(_native.GetSoftwareFramebuffer()); + if (_native == null) + return false; + cb(_native); + _lastRenderedLogicalSize = _savedLogicalSize; + return true; + } + }, (int)w, (int)h, new Vector(dpi, dpi)); } public Action Paint { get; set; } @@ -130,28 +125,6 @@ namespace Avalonia.Native public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); - - class FramebufferWrapper : ILockedFramebuffer - { - public FramebufferWrapper(AvnFramebuffer fb) - { - Address = fb.Data; - Size = new PixelSize(fb.Width, fb.Height); - RowBytes = fb.Stride; - Dpi = new Vector(fb.Dpi.X, fb.Dpi.Y); - Format = (PixelFormat)fb.PixelFormat; - } - public IntPtr Address { get; set; } - public PixelSize Size { get; set; } - public int RowBytes {get;set;} - public Vector Dpi { get; set; } - public PixelFormat Format { get; } - public void Dispose() - { - // Do nothing - } - } - protected class WindowBaseEvents : CallbackBase, IAvnWindowBaseEvents { private readonly WindowBaseImpl _parent; @@ -189,9 +162,12 @@ namespace Avalonia.Native void IAvnWindowBaseEvents.Resized(AvnSize size) { - var s = new Size(size.Width, size.Height); - _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s); + if (_parent._native != null) + { + var s = new Size(size.Width, size.Height); + _parent._savedLogicalSize = s; + _parent.Resized?.Invoke(s); + } } void IAvnWindowBaseEvents.PositionChanged(AvnPoint position) @@ -278,9 +254,7 @@ namespace Avalonia.Native public IRenderer CreateRenderer(IRenderRoot root) { if (_deferredRendering) - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService(), - rendererLock: - _gpu ? new AvaloniaNativeDeferredRendererLock(_native) : null); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); return new ImmediateRenderer(root); } @@ -346,7 +320,7 @@ namespace Avalonia.Native _native.SetTopMost(value); } - public double Scaling => _native.GetScaling(); + public double Scaling => _native?.GetScaling() ?? 1; public Action Deactivated { get; set; } public Action Activated { get; set; } diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index d2e4543af3..a4666bbcbf 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -107,6 +107,7 @@ namespace Avalonia.OpenGL public IGlDisplay Display => _context.Display; public PixelSize Size => _info.Size; public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } } } } diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs index 4b0de05b77..f56f47095e 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs @@ -7,5 +7,6 @@ namespace Avalonia.OpenGL IGlDisplay Display { get; } PixelSize Size { get; } double Scaling { get; } + bool IsYFlipped { get; } } } diff --git a/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs new file mode 100644 index 0000000000..7b28d1a911 --- /dev/null +++ b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Avalonia.Controls +{ + public static class PseudolassesExtensions + { + /// + /// Adds or removes a pseudoclass depending on a boolean value. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// True to add the pseudoclass or false to remove. + public static void Set(this IPseudoClasses classes, string name, bool value) + { + Contract.Requires(classes != null); + + if (value) + { + classes.Add(name); + } + else + { + classes.Remove(name); + } + } + + } +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 13cfa905d4..120a53c664 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -488,83 +488,6 @@ namespace Avalonia InheritanceParent = parent; } - /// - /// Adds a pseudo-class to be set when a property is true. - /// - /// The property. - /// The pseudo-class. - [Obsolete("Use PseudoClass and specify the control type.")] - protected static void PseudoClass(AvaloniaProperty property, string className) - { - PseudoClass(property, className); - } - - /// - /// Adds a pseudo-class to be set when a property is true. - /// - /// The type to apply the pseudo-class to. - /// The property. - /// The pseudo-class. - protected static void PseudoClass(AvaloniaProperty property, string className) - where TOwner : class, IStyledElement - { - PseudoClass(property, x => x, className); - } - - /// - /// Adds a pseudo-class to be set when a property equals a certain value. - /// - /// The type of the property. - /// The property. - /// Returns a boolean value based on the property value. - /// The pseudo-class. - [Obsolete("Use PseudoClass and specify the control type.")] - protected static void PseudoClass( - AvaloniaProperty property, - Func selector, - string className) - { - PseudoClass(property, selector, className); - } - - /// - /// Adds a pseudo-class to be set when a property equals a certain value. - /// - /// The type of the property. - /// The type to apply the pseudo-class to. - /// The property. - /// Returns a boolean value based on the property value. - /// The pseudo-class. - protected static void PseudoClass( - AvaloniaProperty property, - Func selector, - string className) - where TOwner : class, IStyledElement - { - Contract.Requires(property != null); - Contract.Requires(selector != null); - Contract.Requires(className != null); - - if (string.IsNullOrWhiteSpace(className)) - { - throw new ArgumentException("Cannot supply an empty className."); - } - - property.Changed.Merge(property.Initialized) - .Where(e => e.Sender is TOwner) - .Subscribe(e => - { - if (selector((TProperty)e.NewValue)) - { - ((StyledElement)e.Sender).PseudoClasses.Add(className); - } - else - { - ((StyledElement)e.Sender).PseudoClasses.Remove(className); - } - }); - } - protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -650,9 +573,12 @@ namespace Avalonia element._dataContextUpdating = true; element.OnDataContextBeginUpdate(); - foreach (var child in element.LogicalChildren) + var logicalChildren = element.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - if (child is StyledElement s && + if (element.LogicalChildren[i] is StyledElement s && s.InheritanceParent == element && !s.IsSet(DataContextProperty)) { @@ -723,9 +649,15 @@ namespace Avalonia AttachedToLogicalTree?.Invoke(this, e); } - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnAttachedToLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnAttachedToLogicalTreeCore(e); + } } } @@ -738,9 +670,15 @@ namespace Avalonia OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnDetachedFromLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnDetachedFromLogicalTreeCore(e); + } } #if DEBUG diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 9244232ff5..b880ecb01c 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -78,8 +78,6 @@ namespace Avalonia.Styling { Contract.Requires(control != null); - var description = style?.ToString(); - if (Property == null) { throw new InvalidOperationException("Setter.Property must be set."); @@ -107,6 +105,8 @@ namespace Avalonia.Styling } else { + var description = style?.ToString(); + var activated = new ActivatedValue(activator, value, description); return control.Bind(Property, activated, BindingPriority.StyleTrigger); } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 99ee8d8563..22db7adfe4 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -116,13 +116,21 @@ namespace Avalonia.Styling if (match.IsMatch) { var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(Setters.Count + Animations.Count); - if (control is Animatable animatable) + var animatable = control as Animatable; + + var setters = Setters; + var settersCount = setters.Count; + var animations = Animations; + var animationsCount = animations.Count; + + var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); + + if (animatable != null) { - foreach (var animation in Animations) + for (var i = 0; i < animationsCount; i++) { + var animation = animations[i]; var obsMatch = match.Activator; if (match.Result == SelectorMatchResult.AlwaysThisType || @@ -133,17 +141,19 @@ namespace Avalonia.Styling var sub = animation.Apply(animatable, null, obsMatch); subs.Add(sub); - } + } } - foreach (var setter in Setters) + for (var i = 0; i < settersCount; i++) { + var setter = setters[i]; var sub = setter.Apply(this, control, match.Activator); subs.Add(sub); } + subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs))); + controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); } @@ -151,18 +161,23 @@ namespace Avalonia.Styling } else if (control == container) { + var setters = Setters; + var settersCount = setters.Count; + var controlSubscriptions = GetSubscriptions(control); - var subs = new CompositeDisposable(Setters.Count); + var subs = new CompositeDisposable(settersCount + 1); - foreach (var setter in Setters) + for (var i = 0; i < settersCount; i++) { + var setter = setters[i]; var sub = setter.Apply(this, control, null); subs.Add(sub); } + subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs))); + controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); return true; } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 0226288998..fd38c39650 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -239,8 +239,10 @@ namespace Avalonia.Styling /// public bool Remove(IStyle item) => _styles.Remove(item); + public AvaloniaList.Enumerator GetEnumerator() => _styles.GetEnumerator(); + /// - public IEnumerator GetEnumerator() => _styles.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 1c5c8106d3..95bd9550a5 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -4,6 +4,7 @@ + - - + - + @@ -58,7 +58,7 @@ - + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 93989d3782..769ab893bf 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -45,8 +45,7 @@ + IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> @@ -95,8 +94,7 @@ + StaysOpen="True"> 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/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 423d19da14..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}"> - + $"A {Size} {RotationAngle.ToString(CultureInfo.InvariantCulture)} {(IsLargeArc ? 1 : 0)} {(int)SweepDirection} {Point}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/BezierSegment .cs b/src/Avalonia.Visuals/Media/BezierSegment .cs index 8bea03bc23..ddb818c3ac 100644 --- a/src/Avalonia.Visuals/Media/BezierSegment .cs +++ b/src/Avalonia.Visuals/Media/BezierSegment .cs @@ -61,5 +61,8 @@ namespace Avalonia.Media { ctx.CubicBezierTo(Point1, Point2, Point3); } + + public override string ToString() + => $"C {Point1} {Point2} {Point3}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/LineSegment.cs b/src/Avalonia.Visuals/Media/LineSegment.cs index d81f67d99f..81e53ff7c6 100644 --- a/src/Avalonia.Visuals/Media/LineSegment.cs +++ b/src/Avalonia.Visuals/Media/LineSegment.cs @@ -27,5 +27,8 @@ namespace Avalonia.Media { ctx.LineTo(Point); } + + public override string ToString() + => $"L {Point}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathFigure.cs b/src/Avalonia.Visuals/Media/PathFigure.cs index 6a438a85ee..720fca8337 100644 --- a/src/Avalonia.Visuals/Media/PathFigure.cs +++ b/src/Avalonia.Visuals/Media/PathFigure.cs @@ -98,5 +98,8 @@ namespace Avalonia.Media } private PathSegments _segments; + + public override string ToString() + => $"M {StartPoint} {string.Join(" ", _segments)}{(IsClosed ? "Z" : "")}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index 3a91c924d2..be7ffe57a7 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -112,5 +112,9 @@ namespace Avalonia.Media () => InvalidateGeometry()); _figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry()); } + + + public override string ToString() + => $"{(FillRule != FillRule.EvenOdd ? "F1 " : "")}{(string.Join(" ", Figures))}"; } } diff --git a/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs b/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs index 31c364edf4..f1a0ccaaa0 100644 --- a/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs +++ b/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs @@ -42,5 +42,8 @@ namespace Avalonia.Media { ctx.QuadraticBezierTo(Point1, Point2); } + + public override string ToString() + => $"Q {Point1} {Point2}"; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index ef7a254f86..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); + } } } } 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/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 919abae243..94f1dbc8d1 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -995,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/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.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c16b76b539..cea9f88799 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -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_Attached.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs index 44e2976e03..474ef9abec 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs @@ -16,19 +16,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class2.FooProperty)); } - [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,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 4c00d2a1ea..2839fde320 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -132,6 +132,111 @@ namespace Avalonia.Base.UnitTests 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() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 4110c3771f..ca17afb94f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -188,6 +188,7 @@ namespace Avalonia.Base.UnitTests 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); @@ -424,22 +425,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("second", target.Foo); } - [Fact] - public void Property_Notifies_Initialized() - { - bool raised = false; - - Class1.FooProperty.Initialized.Subscribe(e => - raised = e.Property == Class1.FooProperty && - e.OldValue == AvaloniaProperty.UnsetValue && - (string)e.NewValue == "initial" && - e.Priority == BindingPriority.Unset); - - var target = new Class1(); - - Assert.True(raised); - } - [Fact] public void Binding_Error_Reverts_To_Default_Value() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 40631d04cf..4b477287e8 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -30,6 +30,7 @@ namespace Avalonia.Base.UnitTests 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); @@ -239,6 +240,17 @@ namespace Avalonia.Base.UnitTests 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() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 90b8bcff63..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() { @@ -141,11 +123,6 @@ namespace Avalonia.Base.UnitTests OverrideMetadata(typeof(T), metadata); } - internal override void NotifyInitialized(IAvaloniaObject o) - { - throw new NotImplementedException(); - } - internal override IDisposable RouteBind( IAvaloniaObject o, IObservable> source, diff --git a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs index ecf559951a..f29dabb6bf 100644 --- a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs @@ -50,6 +50,18 @@ namespace Avalonia.Base.UnitTests.Data.Converters Assert.Equal(TestEnum.Bar, result); } + [Fact] + public void Can_Convert_String_To_TimeSpan() + { + var result = DefaultValueConverter.Instance.Convert( + "00:00:10", + typeof(TimeSpan), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(TimeSpan.FromSeconds(10), result); + } + [Fact] public void Can_Convert_Int_To_Enum() { diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index c2a8b03f15..8f62dc9136 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -1,33 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -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() { @@ -69,7 +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); } private class Class1 : AvaloniaObject 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/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/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/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.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/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/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 696c0dbf46..004cf009fc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1202,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/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/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.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 389b3c8df8..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); diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e1475dce80..3e589ef3df 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -221,6 +221,48 @@ namespace Avalonia.Visuals.UnitTests.Media context.Verify(v => v.EndFigure(It.IsAny()), Times.AtLeastOnce()); } + [Theory] + [InlineData("M 5.5, 5 L 5.5, 5 L 5.5, 5")] + [InlineData("F1 M 9.0771, 11 C 9.1161, 10.701 9.1801, 10.352 9.3031, 10 L 9.0001, 10 L 9.0001, 6.166 L 3.0001, 9.767 L 3.0001, 10 " + + "L 9.99999999997669E-05, 10 L 9.99999999997669E-05, 0 L 3.0001, 0 L 3.0001, 0.234 L 9.0001, 3.834 L 9.0001, 0 " + + "L 12.0001, 0 L 12.0001, 8.062 C 12.1861, 8.043 12.3821, 8.031 12.5941, 8.031 C 15.3481, 8.031 15.7961, 9.826 " + + "15.9201, 11 L 16.0001, 16 L 9.0001, 16 L 9.0001, 12.562 L 9.0001, 11Z")] + [InlineData("F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")] + [InlineData("M 0, 0 L 10, 10Z")] + [InlineData("M 50, 50 L 100, 100 L 150, 50")] + [InlineData("M 50, 50 L -10, -10 L 10, 50")] + [InlineData("M 50, 50 L 100, 100 L 150, 50Z M 50, 50 L 70, 70 L 120, 50Z")] + [InlineData("M 80, 200 A 100, 50 45 1 0 100, 50")] + [InlineData("F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 " + + "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L " + + "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")] + public void Parsed_Geometry_ToString_Should_Produce_Valid_Value(string pathData) + { + var target = PathGeometry.Parse(pathData); + + string output = target.ToString(); + + Assert.Equal(pathData, output); + } + + [Theory] + [InlineData("M5.5.5 5.5.5 5.5.5", "M 5.5, 0.5 L 5.5, 0.5 L 5.5, 0.5")] + [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z", "F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z")] + [InlineData("F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + + "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + + "12.461,8.046C14.45,8.278,16,9.949,16,12", + "F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 " + + "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L " + + "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12")] + public void Parsed_Geometry_ToString_Should_Format_Value(string pathData, string formattedPathData) + { + var target = PathGeometry.Parse(pathData); + + string output = target.ToString(); + + Assert.Equal(formattedPathData, output); + } + [Theory] [InlineData("0 0")] [InlineData("j")]