diff --git a/Avalonia.sln b/Avalonia.sln index 5bff2fa0a0..3a2c619d5b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -201,7 +201,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject @@ -1896,6 +1898,30 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhone.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|Any CPU.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhone.Build.0 = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -1977,6 +2003,7 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index f9bfaf0b47..1cf3bc75b0 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -26,7 +26,8 @@ struct IAvnStringArray; struct IAvnDndResultCallback; struct IAvnGCHandleDeallocatorCallback; struct IAvnMenuEvents; - +struct IAvnNativeControlHost; +struct IAvnNativeControlHostTopLevelAttachment; enum SystemDecorations { SystemDecorationsNone = 0, SystemDecorationsBorderOnly = 1, @@ -256,6 +257,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) = 0; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; virtual HRESULT SetBlurEnabled (bool enable) = 0; @@ -295,6 +297,7 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0; virtual void ScalingChanged(double scaling) = 0; virtual void RunRenderPriorityJobs() = 0; + virtual void LostFocus() = 0; virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, AvnDragDropEffects effects, IAvnClipboard* clipboard, void* dataObjectHandle) = 0; @@ -478,4 +481,22 @@ AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown virtual void FreeGCHandle(void* handle) = 0; }; +AVNCOM(IAvnNativeControlHost, 20) : IUnknown +{ + virtual HRESULT CreateDefaultChild(void* parent, void** retOut) = 0; + virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() = 0; + virtual void DestroyDefaultChild(void* child) = 0; +}; + +AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown +{ + virtual void* GetParentHandle() = 0; + virtual HRESULT InitializeWithChildHandle(void* child) = 0; + virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0; + virtual void MoveTo(float x, float y, float width, float height) = 0; + virtual void Hide() = 0; + virtual void ReleaseChild() = 0; +}; + + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); 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 ea28780c81..d5cad4d1ca 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 @@ -10,6 +10,8 @@ 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 */; }; + 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; + 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; 1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; }; @@ -32,6 +34,8 @@ 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; }; + 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; + 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; }; 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; }; 1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; }; @@ -86,11 +90,13 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A1852DB23E05814008F0DED /* deadlock.mm */, 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */, + 1AFD334023E03C4F0042899B /* controlhost.mm */, 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, @@ -191,6 +197,7 @@ files = ( 1A002B9E232135EE00021753 /* app.mm in Sources */, 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, + 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, @@ -199,6 +206,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 1e74a70e66..814b91cb62 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -29,9 +29,43 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati @end +@interface AvnApplication : NSApplication + + +@end + +@implementation AvnApplication +{ + BOOL _isHandlingSendEvent; +} + +- (void)sendEvent:(NSEvent *)event +{ + bool oldHandling = _isHandlingSendEvent; + _isHandlingSendEvent = true; + @try { + [super sendEvent: event]; + } @finally { + _isHandlingSendEvent = oldHandling; + } +} + +// This is needed for certain embedded controls +- (BOOL) isHandlingSendEvent +{ + return _isHandlingSendEvent; +} + +- (void)setHandlingSendEvent:(BOOL)handlingSendEvent +{ + _isHandlingSendEvent = handlingSendEvent; +} + +@end + extern void InitializeAvnApp() { - NSApplication* app = [NSApplication sharedApplication]; + NSApplication* app = [AvnApplication sharedApplication]; id delegate = [AvnAppDelegate new]; [app setDelegate:delegate]; } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index df6a7be91c..871bca086d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -24,6 +24,7 @@ extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -54,4 +55,12 @@ template inline T* objc_cast(id from) { - (void) action; @end +class AvnInsidePotentialDeadlock +{ +public: + static bool IsInside(); + AvnInsidePotentialDeadlock(); + ~AvnInsidePotentialDeadlock(); +}; + #endif diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm new file mode 100644 index 0000000000..315ec2f310 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -0,0 +1,145 @@ +#include "common.h" + + +IAvnNativeControlHostTopLevelAttachment* CreateAttachment(); + +class AvnNativeControlHost : + public ComSingleObject +{ +public: + FORWARD_IUNKNOWN(); + NSView* View; + AvnNativeControlHost(NSView* view) + { + View = view; + } + + virtual HRESULT CreateDefaultChild(void* parent, void** retOut) override + { + NSView* view = [NSView new]; + [view setWantsLayer: true]; + + *retOut = (__bridge_retained void*)view; + return S_OK; + }; + + virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() override + { + return ::CreateAttachment(); + }; + + virtual void DestroyDefaultChild(void* child) override + { + // ARC will release the object for us + (__bridge_transfer NSView*) child; + } +}; + +class AvnNativeControlHostTopLevelAttachment : +public ComSingleObject +{ + NSView* _holder; + NSView* _child; +public: + FORWARD_IUNKNOWN(); + + AvnNativeControlHostTopLevelAttachment() + { + _holder = [NSView new]; + [_holder setWantsLayer:true]; + } + + virtual ~AvnNativeControlHostTopLevelAttachment() + { + if(_child != nil && [_child superview] == _holder) + { + [_child removeFromSuperview]; + } + + if([_holder superview] != nil) + { + [_holder removeFromSuperview]; + } + } + + virtual void* GetParentHandle() override + { + return (__bridge void*)_holder; + }; + + virtual HRESULT InitializeWithChildHandle(void* child) override + { + if(_child != nil) + return E_FAIL; + _child = (__bridge NSView*)child; + if(_child == nil) + return E_FAIL; + [_holder addSubview:_child]; + [_child setHidden: false]; + return S_OK; + }; + + virtual HRESULT AttachTo(IAvnNativeControlHost* host) override + { + if(host == nil) + { + [_holder removeFromSuperview]; + [_holder setHidden: true]; + } + else + { + AvnNativeControlHost* chost = dynamic_cast(host); + if(chost == nil || chost->View == nil) + return E_FAIL; + [_holder setHidden:true]; + [chost->View addSubview:_holder]; + } + return S_OK; + }; + + virtual void MoveTo(float x, float y, float width, float height) override + { + if(_child == nil) + return; + if(AvnInsidePotentialDeadlock::IsInside()) + { + IAvnNativeControlHostTopLevelAttachment* slf = this; + slf->AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + slf->MoveTo(x, y, width, height); + slf->Release(); + }); + return; + } + + NSRect childFrame = {0, 0, width, height}; + NSRect holderFrame = {x, y, width, height}; + + [_child setFrame: childFrame]; + [_holder setFrame: holderFrame]; + [_holder setHidden: false]; + if([_holder superview] != nil) + [[_holder superview] setNeedsDisplay:true]; + } + + virtual void Hide() override + { + [_holder setHidden: true]; + } + + virtual void ReleaseChild() override + { + [_child removeFromSuperview]; + _child = nil; + } +}; + +IAvnNativeControlHostTopLevelAttachment* CreateAttachment() +{ + return new AvnNativeControlHostTopLevelAttachment(); +} + +extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent) +{ + return new AvnNativeControlHost(parent); +} diff --git a/native/Avalonia.Native/src/OSX/deadlock.mm b/native/Avalonia.Native/src/OSX/deadlock.mm new file mode 100644 index 0000000000..cb1767c90f --- /dev/null +++ b/native/Avalonia.Native/src/OSX/deadlock.mm @@ -0,0 +1,17 @@ +#include "common.h" + +static int Counter = 0; +AvnInsidePotentialDeadlock::AvnInsidePotentialDeadlock() +{ + Counter++; +} + +AvnInsidePotentialDeadlock::~AvnInsidePotentialDeadlock() +{ + Counter--; +} + +bool AvnInsidePotentialDeadlock::IsInside() +{ + return Counter!=0; +} diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index f826df4da9..86b3584681 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -390,6 +390,14 @@ public: return *ppv == nil ? E_FAIL : S_OK; } + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override + { + if(View == NULL) + return E_FAIL; + *retOut = ::CreateNativeControlHost(View); + return S_OK; + } + virtual HRESULT SetBlurEnabled (bool enable) override { [Window setContentView: enable ? VisualEffect : View]; @@ -1060,9 +1068,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } - - (void)updateLayer { + AvnInsidePotentialDeadlock deadlock; if (_parent == nullptr) { return; @@ -1142,7 +1150,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return; } - [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; auto avnPoint = [self toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; @@ -1169,7 +1176,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; - [self becomeFirstResponder]; + if(type != AvnRawMouseEventType::Move || + ( + [self window] != nil && + ( + [[self window] firstResponder] == nil + || ![[[self window] firstResponder] isKindOfClass: [NSView class]] + ) + ) + ) + [self becomeFirstResponder]; if(_parent != nullptr) { @@ -1179,6 +1195,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super mouseMoved:event]; } +- (BOOL) resignFirstResponder +{ + _parent->BaseEvents->LostFocus(); + return YES; +} + - (void)mouseMoved:(NSEvent *)event { [self mouseEvent:event withType:Move]; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 5460dc7720..890967ae4f 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -101,7 +101,7 @@ partial class Build : NukeBuild .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines .When(Parameters.IsRunningOnWindows && - Parameters.IsRunningOnAzure, c => c + Parameters.IsRunningOnAzure, _ => _ .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) .AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) @@ -176,7 +176,7 @@ partial class Build : NukeBuild .SetFramework(fw) .EnableNoBuild() .EnableNoRestore() - .When(Parameters.PublishTestResults, c => c + .When(Parameters.PublishTestResults, _ => _ .SetLogger("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } diff --git a/nukebuild/Numerge b/nukebuild/Numerge index 4464343aef..aef10ae67d 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8 +Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 2c5a09bee7..cd3ce9adcd 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -41,6 +41,10 @@ true build\ + + true + build\ + diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props index 30bafa37ee..deea3aa391 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.props +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -1,3 +1,11 @@ - + + + + + + + + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 537495fcad..84a62bb5c0 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -4,6 +4,20 @@ <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false low + + + + + %(Filename) + Code + + + %(Filename) + Code + + + + + DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)"> + + + + diff --git a/packages/Avalonia/AvaloniaItemSchema.xaml b/packages/Avalonia/AvaloniaItemSchema.xaml new file mode 100644 index 0000000000..a51ea3c0be --- /dev/null +++ b/packages/Avalonia/AvaloniaItemSchema.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/readme.md b/readme.md index a9263d4816..6a04c7e31e 100644 --- a/readme.md +++ b/readme.md @@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md). ## Contributors -This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. +This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)]. ### Backers diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index 7e945aeaa9..8b697b7948 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -35,6 +35,7 @@ + diff --git a/samples/interop/NativeEmbedSample/App.xaml b/samples/interop/NativeEmbedSample/App.xaml new file mode 100644 index 0000000000..e35ade4087 --- /dev/null +++ b/samples/interop/NativeEmbedSample/App.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/samples/interop/NativeEmbedSample/App.xaml.cs b/samples/interop/NativeEmbedSample/App.xaml.cs new file mode 100644 index 0000000000..cb17cfc35d --- /dev/null +++ b/samples/interop/NativeEmbedSample/App.xaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace NativeEmbedSample +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + desktopLifetime.MainWindow = new MainWindow(); + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/samples/interop/NativeEmbedSample/EmbedSample.cs b/samples/interop/NativeEmbedSample/EmbedSample.cs new file mode 100644 index 0000000000..ab9df11e19 --- /dev/null +++ b/samples/interop/NativeEmbedSample/EmbedSample.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Threading; +using MonoMac.AppKit; +using MonoMac.Foundation; +using MonoMac.WebKit; +using Encoding = SharpDX.Text.Encoding; + +namespace NativeEmbedSample +{ + public class EmbedSample : NativeControlHost + { + public bool IsSecond { get; set; } + private Process _mplayer; + + IPlatformHandle CreateLinux(IPlatformHandle parent) + { + if (IsSecond) + { + var chooser = GtkHelper.CreateGtkFileChooser(parent.Handle); + if (chooser != null) + return chooser; + } + + var control = base.CreateNativeControlCore(parent); + var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName, + "..", + "nodes.mp4")); + _mplayer = Process.Start(new ProcessStartInfo("mplayer", + $"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"") + { + UseShellExecute = false, + + }); + return control; + } + + void DestroyLinux(IPlatformHandle handle) + { + _mplayer?.Kill(); + _mplayer = null; + base.DestroyNativeControlCore(handle); + } + + private const string RichText = + @"{\rtf1\ansi\ansicpg1251\deff0\nouicompat\deflang1049{\fonttbl{\f0\fnil\fcharset0 Calibri;}} +{\colortbl ;\red255\green0\blue0;\red0\green77\blue187;\red0\green176\blue80;\red155\green0\blue211;\red247\green150\blue70;\red75\green172\blue198;} +{\*\generator Riched20 6.3.9600}\viewkind4\uc1 +\pard\sa200\sl276\slmult1\f0\fs22\lang9 I \i am\i0 a \cf1\b Rich Text \cf0\b0\fs24 control\cf2\fs28 !\cf3\fs32 !\cf4\fs36 !\cf1\fs40 !\cf5\fs44 !\cf6\fs48 !\cf0\fs44\par +}"; + + IPlatformHandle CreateWin32(IPlatformHandle parent) + { + WinApi.LoadLibrary("Msftedit.dll"); + var handle = WinApi.CreateWindowEx(0, "RICHEDIT50W", + @"Rich Edit", + 0x800000 | 0x10000000 | 0x40000000 | 0x800000 | 0x10000 | 0x0004, 0, 0, 1, 1, parent.Handle, + IntPtr.Zero, WinApi.GetModuleHandle(null), IntPtr.Zero); + var st = new WinApi.SETTEXTEX { Codepage = 65001, Flags = 0x00000008 }; + var text = RichText.Replace("", IsSecond ? "\\qr " : ""); + var bytes = Encoding.UTF8.GetBytes(text); + WinApi.SendMessage(handle, 0x0400 + 97, ref st, bytes); + return new PlatformHandle(handle, "HWND"); + + } + + void DestroyWin32(IPlatformHandle handle) + { + WinApi.DestroyWindow(handle.Handle); + } + + IPlatformHandle CreateOSX(IPlatformHandle parent) + { + // Note: We are using MonoMac for example purposes + // It shouldn't be used in production apps + MacHelper.EnsureInitialized(); + + var webView = new WebView(); + Dispatcher.UIThread.Post(() => + { + webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl( + IsSecond ? "https://bing.com": "https://google.com/"))); + }); + return new MacOSViewHandle(webView); + + } + + void DestroyOSX(IPlatformHandle handle) + { + ((MacOSViewHandle)handle).Dispose(); + } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return CreateLinux(parent); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return CreateWin32(parent); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return CreateOSX(parent); + return base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + DestroyLinux(control); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + DestroyWin32(control); + else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + DestroyOSX(control); + else + base.DestroyNativeControlCore(control); + } + } +} diff --git a/samples/interop/NativeEmbedSample/GtkHelper.cs b/samples/interop/NativeEmbedSample/GtkHelper.cs new file mode 100644 index 0000000000..e389a51ef5 --- /dev/null +++ b/samples/interop/NativeEmbedSample/GtkHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Platform.Interop; +using Avalonia.X11.NativeDialogs; +using static Avalonia.X11.NativeDialogs.Gtk; +using static Avalonia.X11.NativeDialogs.Glib; +namespace NativeEmbedSample +{ + public class GtkHelper + { + private static Task s_gtkTask; + class FileChooser : INativeControlHostDestroyableControlHandle + { + private readonly IntPtr _widget; + + public FileChooser(IntPtr widget, IntPtr xid) + { + _widget = widget; + Handle = xid; + } + + public IntPtr Handle { get; } + public string HandleDescriptor => "XID"; + public void Destroy() + { + RunOnGlibThread(() => + { + gtk_widget_destroy(_widget); + return 0; + }).Wait(); + } + } + + + + public static IPlatformHandle CreateGtkFileChooser(IntPtr parentXid) + { + if (s_gtkTask == null) + s_gtkTask = StartGtk(); + if (!s_gtkTask.Result) + return null; + return RunOnGlibThread(() => + { + using (var title = new Utf8Buffer("Embedded")) + { + var widget = gtk_file_chooser_dialog_new(title, IntPtr.Zero, GtkFileChooserAction.SelectFolder, + IntPtr.Zero); + gtk_widget_realize(widget); + var xid = gdk_x11_window_get_xid(gtk_widget_get_window(widget)); + gtk_window_present(widget); + return new FileChooser(widget, xid); + } + }).Result; + } + } +} diff --git a/samples/interop/NativeEmbedSample/MacHelper.cs b/samples/interop/NativeEmbedSample/MacHelper.cs new file mode 100644 index 0000000000..74a06a0a0c --- /dev/null +++ b/samples/interop/NativeEmbedSample/MacHelper.cs @@ -0,0 +1,39 @@ +using System; +using Avalonia.Platform; +using MonoMac.AppKit; + +namespace NativeEmbedSample +{ + public class MacHelper + { + private static bool _isInitialized; + + public static void EnsureInitialized() + { + if (_isInitialized) + return; + _isInitialized = true; + NSApplication.Init(); + } + } + + class MacOSViewHandle : IPlatformHandle, IDisposable + { + private NSView _view; + + public MacOSViewHandle(NSView view) + { + _view = view; + } + + public IntPtr Handle => _view?.Handle ?? IntPtr.Zero; + public string HandleDescriptor => "NSView"; + + public void Dispose() + { + _view.Dispose(); + _view = null; + } + } + +} diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml new file mode 100644 index 0000000000..dcec9035e0 --- /dev/null +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + Visible + + + + + + + + Visible + + + + + + diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml.cs b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs new file mode 100644 index 0000000000..4324aa2762 --- /dev/null +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace NativeEmbedSample +{ + public class MainWindow : Window + { + public MainWindow() + { + AvaloniaXamlLoader.Load(this); + this.AttachDevTools(); + } + + public async void ShowPopupDelay(object sender, RoutedEventArgs args) + { + await Task.Delay(3000); + ShowPopup(sender, args); + } + + public void ShowPopup(object sender, RoutedEventArgs args) + { + + new ContextMenu() + { + Items = new List + { + new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" } + } + }.Open((Control)sender); + } + } +} diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj new file mode 100644 index 0000000000..c623ae68b5 --- /dev/null +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -0,0 +1,30 @@ + + + + Exe + netcoreapp2.0 + true + true + + + + + + + + + + + Designer + + + + PreserveNewest + + + + + + + + diff --git a/samples/interop/NativeEmbedSample/Program.cs b/samples/interop/NativeEmbedSample/Program.cs new file mode 100644 index 0000000000..baa7837667 --- /dev/null +++ b/samples/interop/NativeEmbedSample/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; + +namespace NativeEmbedSample +{ + class Program + { + static int Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .With(new AvaloniaNativePlatformOptions() + { + }) + .UsePlatformDetect(); + + } +} diff --git a/samples/interop/NativeEmbedSample/WinApi.cs b/samples/interop/NativeEmbedSample/WinApi.cs new file mode 100644 index 0000000000..8e5bcdf49e --- /dev/null +++ b/samples/interop/NativeEmbedSample/WinApi.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.InteropServices; + +namespace NativeEmbedSample +{ + public unsafe class WinApi + { + public enum CommonControls : uint + { + ICC_LISTVIEW_CLASSES = 0x00000001, // listview, header + ICC_TREEVIEW_CLASSES = 0x00000002, // treeview, tooltips + ICC_BAR_CLASSES = 0x00000004, // toolbar, statusbar, trackbar, tooltips + ICC_TAB_CLASSES = 0x00000008, // tab, tooltips + ICC_UPDOWN_CLASS = 0x00000010, // updown + ICC_PROGRESS_CLASS = 0x00000020, // progress + ICC_HOTKEY_CLASS = 0x00000040, // hotkey + ICC_ANIMATE_CLASS = 0x00000080, // animate + ICC_WIN95_CLASSES = 0x000000FF, + ICC_DATE_CLASSES = 0x00000100, // month picker, date picker, time picker, updown + ICC_USEREX_CLASSES = 0x00000200, // comboex + ICC_COOL_CLASSES = 0x00000400, // rebar (coolbar) control + ICC_INTERNET_CLASSES = 0x00000800, + ICC_PAGESCROLLER_CLASS = 0x00001000, // page scroller + ICC_NATIVEFNTCTL_CLASS = 0x00002000, // native font control + ICC_STANDARD_CLASSES = 0x00004000, + ICC_LINK_CLASS = 0x00008000 + } + + [StructLayout(LayoutKind.Sequential)] + public struct INITCOMMONCONTROLSEX + { + public int dwSize; + public uint dwICC; + } + + [DllImport("Comctl32.dll")] + public static extern void InitCommonControlsEx(ref INITCOMMONCONTROLSEX init); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyWindow(IntPtr hwnd); + + [DllImport("kernel32.dll")] + public static extern IntPtr LoadLibrary(string lib); + + + [DllImport("kernel32.dll")] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + uint dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [StructLayout(LayoutKind.Sequential)] + public struct SETTEXTEX + { + public uint Flags; + public uint Codepage; + } + + [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, ref SETTEXTEX wParam, byte[] lParam); + } +} diff --git a/samples/interop/NativeEmbedSample/nodes-license.md b/samples/interop/NativeEmbedSample/nodes-license.md new file mode 100644 index 0000000000..ab165bb9b6 --- /dev/null +++ b/samples/interop/NativeEmbedSample/nodes-license.md @@ -0,0 +1 @@ +nodes.mp4 by beeple is licensed under the creative commons license, downloaded from https://vimeo.com/9936271 diff --git a/samples/interop/NativeEmbedSample/nodes.mp4 b/samples/interop/NativeEmbedSample/nodes.mp4 new file mode 100644 index 0000000000..5afe23488a Binary files /dev/null and b/samples/interop/NativeEmbedSample/nodes.mp4 differ diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c11cadfbac..2e6f4a67c3 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android throw new NotSupportedException(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index fbe027db00..72732a1f95 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -33,10 +33,8 @@ namespace Avalonia.Android return _view.View.DispatchKeyEvent(e); } - class ViewImpl : TopLevelImpl, IEmbeddableWindowImpl + class ViewImpl : TopLevelImpl { - public event Action LostFocus; - public ViewImpl(Context context) : base(context) { View.Focusable = true; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 71706676d6..69fd2a9f13 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -194,6 +194,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } public IPopupImpl CreatePopup() => null; + + public Action LostFocus { get; set; } ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface); } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index a0868152f6..aa4fe55f79 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -49,6 +50,18 @@ namespace Avalonia.Data.Core var accessor = plugin?.Start(reference, PropertyName); + // We need to handle accessor fallback before handling validation. Validators do not support null accessors. + if (accessor == null) + { + reference.TryGetTarget(out object instance); + + var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; + + var exception = new MissingMemberException(message); + + accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); + } + if (_enableValidation && Next == null) { foreach (var validator in ExpressionObserver.DataValidators) @@ -60,15 +73,9 @@ namespace Avalonia.Data.Core } } - if (accessor == null) + if (accessor is null) { - reference.TryGetTarget(out object instance); - - var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'"; - - var exception = new MissingMemberException(message); - - accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); + throw new AvaloniaInternalException("Data validators must return non-null accessor."); } _accessor = accessor; diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index ebafc8b946..56cde9738e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -14,7 +14,7 @@ namespace Avalonia.Threading private readonly DispatcherPriority _priority; private TimeSpan _interval; - + /// /// Initializes a new instance of the class. /// @@ -154,6 +154,8 @@ namespace Avalonia.Threading TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) { + interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); + var timer = new DispatcherTimer(priority) { Interval = interval }; timer.Tick += (s, e) => @@ -197,7 +199,7 @@ namespace Avalonia.Threading } } - + /// /// Raises the event on the dispatcher thread. diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 406abe6f99..ae2bf99d1e 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks foreach (var s in sources.ToList()) { - if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml")) + if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) { XamlFileInfo info; try @@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); - foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 3b69109e68..30e8f120d7 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -25,7 +25,8 @@ namespace Avalonia.Build.Tasks public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") - || r.Name.ToLowerInvariant().EndsWith(".paml"); + || r.Name.ToLowerInvariant().EndsWith(".paml") + || r.Name.ToLowerInvariant().EndsWith(".axaml"); public class CompileResult { diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 2d48a7d33b..5ff0fd1feb 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -10,7 +10,7 @@ namespace Avalonia.Controls.Embedding { public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable { - public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) + public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl) { } @@ -19,16 +19,13 @@ namespace Avalonia.Controls.Embedding { } - [CanBeNull] - public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl; - protected bool EnforceClientSize { get; set; } = true; public void Prepare() { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index d326ab5734..b037dd9901 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 09f9ed7c1b..48fc8d182f 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -63,6 +63,7 @@ namespace Avalonia.Controls.Embedding.Offscreen } public Action Closed { get; set; } + public Action LostFocus { get; set; } public abstract IMouseDevice MouseDevice { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs new file mode 100644 index 0000000000..94ef0b2284 --- /dev/null +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -0,0 +1,141 @@ +using Avalonia.Controls.Platform; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Controls +{ + public class NativeControlHost : Control + { + private TopLevel _currentRoot; + private INativeControlHostImpl _currentHost; + private INativeControlHostControlTopLevelAttachment _attachment; + private IPlatformHandle _nativeControlHandle; + private bool _queuedForDestruction; + static NativeControlHost() + { + IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); + TransformedBoundsProperty.Changed.AddClassHandler(OnBoundsChanged); + } + + private static void OnBoundsChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) + => host.UpdateHost(); + + private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) + => host.UpdateHost(); + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = e.Root as TopLevel; + UpdateHost(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = null; + UpdateHost(); + } + + + void UpdateHost() + { + _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; + var needsAttachment = _currentHost != null; + var needsShow = needsAttachment && IsEffectivelyVisible && TransformedBounds.HasValue; + + if (needsAttachment) + { + // If there is an existing attachment, ensure that we are attached to the proper host or destroy the attachment + if (_attachment != null && _attachment.AttachedTo != _currentHost) + { + if (_attachment != null) + { + if (_attachment.IsCompatibleWith(_currentHost)) + { + _attachment.AttachedTo = _currentHost; + } + else + { + _attachment.Dispose(); + _attachment = null; + } + } + } + + // If there is no attachment, but the control exists, + // attempt to attach to to the current toplevel or destroy the control if it's incompatible + if (_attachment == null && _nativeControlHandle != null) + { + if (_currentHost.IsCompatibleWith(_nativeControlHandle)) + _attachment = _currentHost.CreateNewAttachment(_nativeControlHandle); + else + DestroyNativeControl(); + } + + // There is no control handle an no attachment, create both + if (_nativeControlHandle == null) + { + _attachment = _currentHost.CreateNewAttachment(parent => + _nativeControlHandle = CreateNativeControlCore(parent)); + } + } + else + { + // Immediately detach the control from the current toplevel if there is an existing attachment + if (_attachment != null) + _attachment.AttachedTo = null; + + // Don't destroy the control immediately, it might be just being reparented to another TopLevel + if (_nativeControlHandle != null && !_queuedForDestruction) + { + _queuedForDestruction = true; + Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background); + } + } + + if (needsShow) + _attachment?.ShowInBounds(TransformedBounds.Value); + else if (needsAttachment) + _attachment?.Hide(); + } + + public bool TryUpdateNativeControlPosition() + { + var needsShow = _currentHost != null && IsEffectivelyVisible && TransformedBounds.HasValue; + + if(needsShow) + _attachment?.ShowInBounds(TransformedBounds.Value); + return needsShow; + } + + void CheckDestruction() + { + _queuedForDestruction = false; + if (_currentRoot == null) + DestroyNativeControl(); + } + + protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + return _currentHost.CreateDefaultChild(parent); + } + + void DestroyNativeControl() + { + if (_nativeControlHandle != null) + { + _attachment?.Dispose(); + _attachment = null; + + DestroyNativeControlCore(_nativeControlHandle); + _nativeControlHandle = null; + } + } + + protected virtual void DestroyNativeControlCore(IPlatformHandle control) + { + ((INativeControlHostDestroyableControlHandle)control).Destroy(); + } + + } +} diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index e61e88c22b..2a7ea12d79 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -67,6 +68,9 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } + if (_root is TopLevel tl) + tl.PlatformImpl.LostFocus += TopLevelLostPlatformFocus; + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } @@ -96,6 +100,9 @@ namespace Avalonia.Controls.Platform { root.Deactivated -= WindowDeactivated; } + + if (_root is TopLevel tl) + tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus; _inputManagerSubscription?.Dispose(); @@ -333,6 +340,10 @@ namespace Avalonia.Controls.Platform { item.Parent.SelectedItem = null; } + else if (!item.IsPointerOverSubMenu) + { + item.IsSubMenuOpen = false; + } } } @@ -405,6 +416,11 @@ namespace Avalonia.Controls.Platform { Menu?.Close(); } + + private void TopLevelLostPlatformFocus() + { + Menu?.Close(); + } protected void Click(IMenuItem item) { diff --git a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs b/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs deleted file mode 100644 index e2d174d9b6..0000000000 --- a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - /// - /// Defines a platform-specific embeddable window implementation. - /// - public interface IEmbeddableWindowImpl : ITopLevelImpl - { - event Action LostFocus; - } -} diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs new file mode 100644 index 0000000000..7a4568abc6 --- /dev/null +++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Platform +{ + public interface INativeControlHostImpl + { + INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent); + INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create); + INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle); + bool IsCompatibleWith(IPlatformHandle handle); + } + + public interface INativeControlHostDestroyableControlHandle : IPlatformHandle + { + void Destroy(); + } + + public interface INativeControlHostControlTopLevelAttachment : IDisposable + { + INativeControlHostImpl AttachedTo { get; set; } + bool IsCompatibleWith(INativeControlHostImpl host); + void Hide(); + void ShowInBounds(TransformedBounds transformedBounds); + } + + public interface ITopLevelImplWithNativeControlHost + { + INativeControlHostImpl NativeControlHost { get; } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index c7875f6413..6472fba48e 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -104,6 +104,11 @@ namespace Avalonia.Platform /// Gets or sets a method called when the underlying implementation is destroyed. /// Action Closed { get; set; } + + /// + /// Gets or sets a method called when the input focus is lost. + /// + Action LostFocus { get; set; } /// /// Gets a mouse device associated with toplevel diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index a55bd63c6a..be8939e19a 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -3,6 +3,6 @@ namespace Avalonia.Platform public interface IWindowingPlatform { IWindowImpl CreateWindow(); - IEmbeddableWindowImpl CreateEmbeddableWindow(); + IWindowImpl CreateEmbeddableWindow(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index ef453274b8..19d034b4e2 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Platform return s_designerMode ? (IWindowImpl)platform.CreateEmbeddableWindow() : platform.CreateWindow(); } - public static IEmbeddableWindowImpl CreateEmbeddableWindow() + public static IWindowImpl CreateEmbeddableWindow() { var platform = AvaloniaLocator.Current.GetService(); if (platform == null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ac4f805174..1fcf8d61bc 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -9,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Platform; using Avalonia.VisualTree; #nullable enable @@ -351,6 +352,10 @@ namespace Avalonia.Controls.Primitives DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated, (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler)); + + DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, + (x, handler) => x.LostFocus += handler, + (x, handler) => x.LostFocus -= handler)); } else { @@ -610,6 +615,12 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private void WindowLostFocus() + { + if(!StaysOpen) + Close(); + } private IgnoreIsOpenScope BeginIgnoringIsOpen() { diff --git a/src/Avalonia.Controls/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index e116316904..4f5a7cd311 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -10,13 +10,13 @@ namespace Avalonia.Controls.Remote { private EmbeddableControlRoot _topLevel; - class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl, IEmbeddableWindowImpl + class EmbeddableRemoteServerTopLevelImpl : RemoteServerTopLevelImpl { public EmbeddableRemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) : base(transport) { } #pragma warning disable 67 - public event Action LostFocus; + public Action LostFocus { get; set; } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index b705a518ff..0d22187b34 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -49,8 +49,8 @@ namespace Avalonia.Controls // For non-virtualizing layouts, we do not need to keep // updating viewports and invalidating measure often. So when // a non virtualizing layout is used, we stop doing all that work. - bool _managingViewportDisabled; - private IDisposable _effectiveViewportChangedRevoker; + private bool _managingViewportDisabled; + private bool _effectiveViewportChangedSubscribed; private bool _layoutUpdatedSubscribed; public ViewportManager(ItemsRepeater owner) @@ -228,11 +228,15 @@ namespace Avalonia.Controls _pendingViewportShift = default; _unshiftableShift = default; - _effectiveViewportChangedRevoker?.Dispose(); - - if (!_managingViewportDisabled) + if (_managingViewportDisabled && _effectiveViewportChangedSubscribed) { - _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = false; + } + else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed) + { + _owner.EffectiveViewportChanged += OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = true; } } @@ -340,6 +344,11 @@ namespace Avalonia.Controls // Note that the element being brought into view could be a descendant. var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + if (targetChild is null) + { + return; + } + // Make sure that only the target child can be the anchor during the bring into view operation. foreach (var child in _owner.Children) { @@ -373,7 +382,7 @@ namespace Avalonia.Controls if (parent == null) { - throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + return null; } return targetChild; @@ -415,15 +424,15 @@ namespace Avalonia.Controls _scroller = null; } - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = null; + _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = false; _ensuredScroller = false; } - private void OnEffectiveViewportChanged(Rect effectiveViewport) + private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); - UpdateViewport(effectiveViewport); + UpdateViewport(e.EffectiveViewport); _pendingViewportShift = default; _unshiftableShift = default; @@ -468,8 +477,8 @@ namespace Avalonia.Controls } else if (!_managingViewportDisabled) { - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + _owner.EffectiveViewportChanged += OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = true; } _ensuredScroller = true; @@ -529,38 +538,6 @@ namespace Avalonia.Controls } } - private IDisposable SubscribeToEffectiveViewportChanged(IControl control) - { - // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - - // we can get this from TransformedBounds, but this property is updated after layout has - // run, which is too late. Instead, for now lets just hook into an internal event on - // ScrollContentPresenter to find out what the offset and viewport will be after arrange - // and use those values. Note that this doesn't handle nested ScrollViewers at all, but - // it's enough to get scrolling to non-uniformly sized items working for now. - // - // UWP uses the EffectiveViewportChanged event (which I think was implemented specially - // for this case): we need to implement that in Avalonia, but the semantics of it aren't - // clear to me. Hopefully the source for this event will be released with WinUI 3. - if (control.VisualParent is ScrollContentPresenter scp) - { - scp.PreArrange += ScrollContentPresenterPreArrange; - return Disposable.Create(() => scp.PreArrange -= ScrollContentPresenterPreArrange); - } - - return Disposable.Empty; - } - - private void ScrollContentPresenterPreArrange(object sender, VectorEventArgs e) - { - var scp = (ScrollContentPresenter)sender; - var effectiveViewport = new Rect((Point)scp.Offset, new Size(e.Vector.X, e.Vector.Y)); - - if (effectiveViewport != _visibleWindow) - { - OnEffectiveViewportChanged(effectiveViewport); - } - } - private class ScrollerInfo { public ScrollerInfo(ScrollViewer scroller) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d34444eb8..6c3a2d4a03 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -11,6 +11,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; +using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -170,6 +171,8 @@ namespace Avalonia.Controls nameof(IResourceHost.ResourcesChanged), this); } + + impl.LostFocus += PlatformImpl_LostFocus; } /// @@ -315,7 +318,7 @@ namespace Avalonia.Controls /// /// Creates the layout manager for this . /// - protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(); + protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); /// /// Handles a paint notification from . @@ -348,6 +351,7 @@ namespace Avalonia.Controls OnClosed(EventArgs.Empty); Renderer?.Dispose(); Renderer = null; + LayoutManager?.Dispose(); } /// @@ -471,5 +475,17 @@ namespace Avalonia.Controls { (this as IInputRoot).MouseDevice.SceneInvalidated(this, e.DirtyRect); } + + void PlatformImpl_LostFocus() + { + var focused = (IVisual)FocusManager.Instance.Current; + if (focused == null) + return; + while (focused.VisualParent != null) + focused = focused.VisualParent; + + if (focused == this) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); + } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ff7cc41e3b..cedd20ace5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -519,7 +519,7 @@ namespace Avalonia.Controls } } - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); using (BeginAutoSizing()) { @@ -592,7 +592,7 @@ namespace Avalonia.Controls } } - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); var result = new TaskCompletionSource(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index afc01db506..eb6e7319f5 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls if (!_hasExecutedInitialLayoutPass) { - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } PlatformImpl?.Show(); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index cd64af60e2..d42eda6e5e 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -10,7 +10,7 @@ using Avalonia.Threading; namespace Avalonia.DesignerSupport.Remote { - class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, IEmbeddableWindowImpl + class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl { private readonly IAvaloniaRemoteTransportConnection _transport; @@ -45,11 +45,6 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } public Size MaxAutoSizeHint { get; } = new Size(4096, 4096); - public event Action LostFocus - { - add {} - remove {} - } protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) { diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index dcfcd42c04..fe4c580bbb 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote public IWindowImpl CreateWindow() => new WindowStub(); - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { if (s_lastWindow != null) { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index b001bc1b76..39b8d7f076 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -29,6 +29,7 @@ namespace Avalonia.DesignerSupport.Remote public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } + public Action LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); public IPopupImpl CreatePopup() => new WindowStub(this); diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index 28c0fe8df6..722215f8b7 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -30,6 +30,19 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); + + /// + /// Defines the IsTabStop attached property. + /// + /// + /// The IsTabStop attached property determines whether the control is focusable by tab navigation. + /// + public static readonly AttachedProperty IsTabStopProperty = + AvaloniaProperty.RegisterAttached( + "IsTabStop", + typeof(KeyboardNavigation), + true); + /// /// Gets the for a container. /// @@ -69,5 +82,25 @@ namespace Avalonia.Input { element.SetValue(TabOnceActiveElementProperty, value); } + + /// + /// Sets the for a container. + /// + /// The container. + /// Value indicating whether the container is a tab stop. + public static void SetIsTabStop(InputElement element, bool value) + { + element.SetValue(IsTabStopProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// Whether the container is a tab stop. + public static bool GetIsTabStop(InputElement element) + { + return element.GetValue(IsTabStopProperty); + } } } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 596395eac6..dd50ea438a 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -77,7 +77,8 @@ namespace Avalonia.Input.Navigation /// The element. /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) + private static IEnumerable GetFocusableDescendants(IInputElement element, + NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -113,7 +114,7 @@ namespace Avalonia.Input.Navigation } else { - if (child.CanFocus()) + if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) { yield return child; } @@ -122,7 +123,10 @@ namespace Avalonia.Input.Navigation { foreach (var descendant in GetFocusableDescendants(child, direction)) { - yield return descendant; + if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) + { + yield return descendant; + } } } } @@ -167,7 +171,9 @@ namespace Avalonia.Input.Navigation { element = navigable.GetControl(direction, element, false); - if (element != null && element.CanFocus()) + if (element != null && + element.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) element)) { break; } @@ -233,26 +239,22 @@ namespace Avalonia.Input.Navigation return customNext.next; } - if (sibling.CanFocus()) + if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) { return sibling; } - else + + next = direction == NavigationDirection.Next ? + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); + + if (next != null) { - next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling, direction).FirstOrDefault() : - GetFocusableDescendants(sibling, direction).LastOrDefault(); - if(next != null) - { - return next; - } + return next; } } - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } + next = GetFirstInNextContainer(element, parent, direction); } else { @@ -264,7 +266,8 @@ namespace Avalonia.Input.Navigation return next; } - private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, + NavigationDirection direction) { if (element is ICustomKeyboardNavigation custom) { diff --git a/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs new file mode 100644 index 0000000000..1cdc775b13 --- /dev/null +++ b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Layout +{ + /// + /// Provides data for the event. + /// + public class EffectiveViewportChangedEventArgs : EventArgs + { + public EffectiveViewportChangedEventArgs(Rect effectiveViewport) + { + EffectiveViewport = effectiveViewport; + } + + /// + /// Gets the representing the effective viewport. + /// + /// + /// The viewport is expressed in coordinates relative to the control that the event is + /// raised on. + /// + public Rect EffectiveViewport { get; } + } +} diff --git a/src/Avalonia.Layout/ILayoutManager.cs b/src/Avalonia.Layout/ILayoutManager.cs index 6e63d3edbb..614670a53b 100644 --- a/src/Avalonia.Layout/ILayoutManager.cs +++ b/src/Avalonia.Layout/ILayoutManager.cs @@ -7,7 +7,7 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - public interface ILayoutManager + public interface ILayoutManager : IDisposable { /// /// Raised when the layout manager completes a layout pass. @@ -35,6 +35,15 @@ namespace Avalonia.Layout /// void ExecuteLayoutPass(); + /// + /// Executes the initial layout pass on a layout root. + /// + /// + /// You should not usually need to call this method explictly, the layout root will call + /// it to carry out the initial layout of the control. + /// + void ExecuteInitialLayoutPass(); + /// /// Executes the initial layout pass on a layout root. /// @@ -43,6 +52,19 @@ namespace Avalonia.Layout /// You should not usually need to call this method explictly, the layout root will call /// it to carry out the initial layout of the control. /// + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] void ExecuteInitialLayoutPass(ILayoutRoot root); + + /// + /// Registers a control as wanting to receive effective viewport notifications. + /// + /// The control. + void RegisterEffectiveViewportListener(ILayoutable control); + + /// + /// Registers a control as no longer wanting to receive effective viewport notifications. + /// + /// The control. + void UnregisterEffectiveViewportListener(ILayoutable control); } } diff --git a/src/Avalonia.Layout/ILayoutable.cs b/src/Avalonia.Layout/ILayoutable.cs index 316a017f1d..54d3ba6a11 100644 --- a/src/Avalonia.Layout/ILayoutable.cs +++ b/src/Avalonia.Layout/ILayoutable.cs @@ -111,5 +111,12 @@ namespace Avalonia.Layout /// /// The child control. void ChildDesiredSizeChanged(ILayoutable control); + + /// + /// Used by the to notify the control that its effective + /// viewport is changed. + /// + /// The viewport information. + void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e); } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index aefb319fd0..792de774d1 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -1,7 +1,10 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; +using Avalonia.VisualTree; #nullable enable @@ -10,16 +13,21 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - public class LayoutManager : ILayoutManager + public class LayoutManager : ILayoutManager, IDisposable { + private const int MaxPasses = 3; + private readonly ILayoutRoot _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private readonly Action _executeLayoutPass; + private List? _effectiveViewportChangedListeners; + private bool _disposed; private bool _queued; private bool _running; - public LayoutManager() + public LayoutManager(ILayoutRoot owner) { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _executeLayoutPass = ExecuteLayoutPass; } @@ -31,6 +39,11 @@ namespace Avalonia.Layout control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -41,6 +54,11 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager."); + } + _toMeasure.Enqueue(control); _toArrange.Enqueue(control); QueueLayoutPass(); @@ -52,6 +70,11 @@ namespace Avalonia.Layout control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -62,6 +85,11 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); + } + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -69,10 +97,13 @@ namespace Avalonia.Layout /// public virtual void ExecuteLayoutPass() { - const int MaxPasses = 3; - Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!_running) { _running = true; @@ -97,23 +128,15 @@ namespace Avalonia.Layout _toMeasure.BeginLoop(MaxPasses); _toArrange.BeginLoop(MaxPasses); - try + for (var pass = 0; pass < MaxPasses; ++pass) { - for (var pass = 0; pass < MaxPasses; ++pass) - { - ExecuteMeasurePass(); - ExecuteArrangePass(); + InnerLayoutPass(); - if (_toMeasure.Count == 0) - { - break; - } + if (!RaiseEffectiveViewportChanged()) + { + break; } } - finally - { - _running = false; - } _toMeasure.EndLoop(); _toArrange.EndLoop(); @@ -131,13 +154,18 @@ namespace Avalonia.Layout } /// - public virtual void ExecuteInitialLayoutPass(ILayoutRoot root) + public virtual void ExecuteInitialLayoutPass() { + if (_disposed) + { + return; + } + try { _running = true; - Measure(root); - Arrange(root); + Measure(_owner); + Arrange(_owner); } finally { @@ -151,6 +179,67 @@ namespace Avalonia.Layout ExecuteLayoutPass(); } + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] + public void ExecuteInitialLayoutPass(ILayoutRoot root) + { + if (root != _owner) + { + throw new ArgumentException("ExecuteInitialLayoutPass called with incorrect root."); + } + + ExecuteInitialLayoutPass(); + } + + public void Dispose() + { + _disposed = true; + _toMeasure.Dispose(); + _toArrange.Dispose(); + } + + void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control) + { + _effectiveViewportChangedListeners ??= new List(); + _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener( + control, + CalculateEffectiveViewport(control))); + } + + void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control) + { + if (_effectiveViewportChangedListeners is object) + { + for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i) + { + if (_effectiveViewportChangedListeners[i].Listener == control) + { + _effectiveViewportChangedListeners.RemoveAt(i); + } + } + } + } + + private void InnerLayoutPass() + { + try + { + for (var pass = 0; pass < MaxPasses; ++pass) + { + ExecuteMeasurePass(); + ExecuteArrangePass(); + + if (_toMeasure.Count == 0) + { + break; + } + } + } + finally + { + _running = false; + } + } + private void ExecuteMeasurePass() { while (_toMeasure.Count > 0) @@ -234,5 +323,97 @@ namespace Avalonia.Layout _queued = true; } } + + private bool RaiseEffectiveViewportChanged() + { + var startCount = _toMeasure.Count + _toArrange.Count; + + if (_effectiveViewportChangedListeners is object) + { + var count = _effectiveViewportChangedListeners.Count; + var pool = ArrayPool.Shared; + var listeners = pool.Rent(count); + + _effectiveViewportChangedListeners.CopyTo(listeners); + + try + { + for (var i = 0; i < count; ++i) + { + var l = _effectiveViewportChangedListeners[i]; + + if (!l.Listener.IsAttachedToVisualTree) + { + continue; + } + + var viewport = CalculateEffectiveViewport(l.Listener); + + if (viewport != l.Viewport) + { + l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); + _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + } + } + } + finally + { + pool.Return(listeners, clearArray: true); + } + } + + return startCount != _toMeasure.Count + _toMeasure.Count; + } + + private Rect CalculateEffectiveViewport(IVisual control) + { + var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity); + CalculateEffectiveViewport(control, control, ref viewport); + return viewport; + } + + private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport) + { + // Recurse until the top level control. + if (control.VisualParent is object) + { + CalculateEffectiveViewport(target, control.VisualParent, ref viewport); + } + else + { + viewport = new Rect(control.Bounds.Size); + } + + // Apply the control clip bounds if it's not the target control. We don't apply it to + // the target control because it may itself be clipped to bounds and if so the viewport + // we calculate would be of no use. + if (control != target && control.ClipToBounds) + { + viewport = control.Bounds.Intersect(viewport); + } + + // Translate the viewport into this control's coordinate space. + viewport = viewport.Translate(-control.Bounds.Position); + + if (control != target && control.RenderTransform is object) + { + var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size); + var offset = Matrix.CreateTranslation(origin); + var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset); + viewport = viewport.TransformToAABB(renderTransform); + } + } + + private readonly struct EffectiveViewportChangedListener + { + public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) + { + Listener = listener; + Viewport = viewport; + } + + public ILayoutable Listener { get; } + public Rect Viewport { get; } + } } } diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs index e261a1b48e..1a9eb6b785 100644 --- a/src/Avalonia.Layout/LayoutQueue.cs +++ b/src/Avalonia.Layout/LayoutQueue.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Avalonia.Layout { - internal class LayoutQueue : IReadOnlyCollection + internal class LayoutQueue : IReadOnlyCollection, IDisposable { private struct Info { @@ -84,5 +84,12 @@ namespace Avalonia.Layout _notFinalizedBuffer.Clear(); } + + public void Dispose() + { + _inner.Clear(); + _loopQueueInfo.Clear(); + _notFinalizedBuffer.Clear(); + } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 8d2a825fa0..e62e22f8ec 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -132,6 +132,7 @@ namespace Avalonia.Layout private bool _measuring; private Size? _previousMeasure; private Rect? _previousArrange; + private EventHandler? _effectiveViewportChanged; private EventHandler? _layoutUpdated; /// @@ -152,6 +153,32 @@ namespace Avalonia.Layout VerticalAlignmentProperty); } + /// + /// Occurs when the element's effective viewport changes. + /// + public event EventHandler? EffectiveViewportChanged + { + add + { + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } + + _effectiveViewportChanged += value; + } + + remove + { + _effectiveViewportChanged -= value; + + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } + } + } + /// /// Occurs when a layout pass completes for the control. /// @@ -384,13 +411,6 @@ namespace Avalonia.Layout } } - /// - /// Called by InvalidateMeasure - /// - protected virtual void OnMeasureInvalidated() - { - } - /// /// Invalidates the measurement of the control and queues a new layout pass. /// @@ -436,6 +456,11 @@ namespace Avalonia.Layout } } + void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e) + { + _effectiveViewportChanged?.Invoke(this, e); + } + /// /// Marks a property as affecting the control's measurement. /// @@ -717,9 +742,17 @@ namespace Avalonia.Layout { base.OnAttachedToVisualTreeCore(e); - if (_layoutUpdated is object && e.Root is ILayoutRoot r) + if (e.Root is ILayoutRoot r) { - r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } } } @@ -727,12 +760,27 @@ namespace Avalonia.Layout { base.OnDetachedFromVisualTreeCore(e); - if (_layoutUpdated is object && e.Root is ILayoutRoot r) + if (e.Root is ILayoutRoot r) { - r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } } } + /// + /// Called by InvalidateMeasure + /// + protected virtual void OnMeasureInvalidated() + { + } + /// protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index e46f4d4a15..cfd47d48de 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -122,7 +122,7 @@ namespace Avalonia.Native return new WindowImpl(_factory, _options, _glFeature); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/src/Avalonia.Native/NativeControlHostImpl.cs b/src/Avalonia.Native/NativeControlHostImpl.cs new file mode 100644 index 0000000000..0777c6416b --- /dev/null +++ b/src/Avalonia.Native/NativeControlHostImpl.cs @@ -0,0 +1,136 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class NativeControlHostImpl : IDisposable, INativeControlHostImpl + { + private IAvnNativeControlHost _host; + + public NativeControlHostImpl(IAvnNativeControlHost host) + { + _host = host; + } + + public void Dispose() + { + _host?.Dispose(); + _host = null; + } + + class DestroyableNSView : INativeControlHostDestroyableControlHandle + { + private IAvnNativeControlHost _impl; + private IntPtr _nsView; + + public DestroyableNSView(IAvnNativeControlHost impl) + { + _impl = new IAvnNativeControlHost(impl.NativePointer); + _impl.AddRef(); + _nsView = _impl.CreateDefaultChild(IntPtr.Zero); + } + + public IntPtr Handle => _nsView; + public string HandleDescriptor => "NSView"; + public void Destroy() + { + if (_impl != null) + { + _impl.DestroyDefaultChild(_nsView); + _impl.Dispose(); + _impl = null; + _nsView = IntPtr.Zero; + } + } + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + => new DestroyableNSView(_host); + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment( + Func create) + { + var a = new Attachment(_host.CreateAttachment()); + try + { + var child = create(a.GetParentHandle()); + a.InitWithChild(child); + a.AttachedTo = this; + return a; + } + catch + { + a.Dispose(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + var a = new Attachment(_host.CreateAttachment()); + a.InitWithChild(handle); + a.AttachedTo = this; + return a; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "NSView"; + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private IAvnNativeControlHostTopLevelAttachment _native; + private NativeControlHostImpl _attachedTo; + public IPlatformHandle GetParentHandle() => new PlatformHandle(_native.ParentHandle, "NSView"); + public Attachment(IAvnNativeControlHostTopLevelAttachment native) + { + _native = native; + } + + public void Dispose() + { + if (_native != null) + { + _native.ReleaseChild(); + _native.Dispose(); + _native = null; + } + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + var host = (NativeControlHostImpl)value; + if(host == null) + _native.AttachTo(null); + else + _native.AttachTo(host._host); + _attachedTo = host; + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl; + + public void Hide() + { + _native?.Hide(); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + if (_attachedTo == null) + throw new InvalidOperationException("Native control isn't attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform); + bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), + Math.Max(1, bounds.Height)); + _native.MoveTo((float) bounds.X, (float) bounds.Y, (float) bounds.Width, (float) bounds.Height); + } + + public void InitWithChild(IPlatformHandle handle) + => _native.InitializeWithChildHandle(handle.Handle); + } + } +} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 9783454e0e..930b5800ba 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; @@ -43,7 +44,7 @@ namespace Avalonia.Native } public abstract class WindowBaseImpl : IWindowBaseImpl, - IFramebufferPlatformSurface + IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost { IInputRoot _inputRoot; IAvnWindowBase _native; @@ -57,6 +58,7 @@ namespace Avalonia.Native private Size _lastRenderedLogicalSize; private double _savedScaling; private GlPlatformSurface _glSurface; + private NativeControlHostImpl _nativeControlHost; private IGlContext _glContext; internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature) @@ -80,6 +82,7 @@ namespace Avalonia.Native Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; _savedScaling = Scaling; + _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); @@ -101,6 +104,8 @@ namespace Avalonia.Native this }; + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + public ILockedFramebuffer Lock() { var w = _savedLogicalSize.Width * _savedScaling; @@ -119,6 +124,8 @@ namespace Avalonia.Native }, (int)w, (int)h, new Vector(dpi, dpi)); } + public Action LostFocus { get; set; } + public Action Paint { get; set; } public Action Resized { get; set; } public Action Closed { get; set; } @@ -201,6 +208,11 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + void IAvnWindowBaseEvents.LostFocus() + { + _parent.LostFocus?.Invoke(); + } public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, AvnInputModifiers modifiers, @@ -288,6 +300,9 @@ namespace Avalonia.Native _native?.Dispose(); _native = null; + _nativeControlHost?.Dispose(); + _nativeControlHost = null; + (Screen as ScreenImpl)?.Dispose(); } diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index a4eab83e4a..84bf799d8d 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml new file mode 100644 index 0000000000..f0f3e5ea16 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -0,0 +1,23 @@ + + + + diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 8776d3a7b7..a8e618af27 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -1,6 +1,5 @@ using System; using System.Reactive.Disposables; -using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -11,9 +10,9 @@ namespace Avalonia.Animation.Animators /// public class SolidColorBrushAnimator : Animator { - ColorAnimator _colorAnimator; + private ColorAnimator _colorAnimator; - void InitializeColorAnimator() + private void InitializeColorAnimator() { _colorAnimator = new ColorAnimator(); @@ -27,46 +26,44 @@ namespace Avalonia.Animation.Animators public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { + // Preprocess keyframe values to Color if the xaml parser converts them to ISCB. foreach (var keyframe in this) { - if (keyframe.Value as ISolidColorBrush == null) - return Disposable.Empty; - - // Preprocess keyframe values to Color if the xaml parser converts them to ISCB. - if (keyframe.Value.GetType() == typeof(ImmutableSolidColorBrush)) + if (keyframe.Value is ISolidColorBrush colorBrush) { - keyframe.Value = ((ImmutableSolidColorBrush)keyframe.Value).Color; + keyframe.Value = colorBrush.Color; + } + else + { + return Disposable.Empty; } } - // Add SCB if the target prop is empty. - if (control.GetValue(Property) == null) - control.SetValue(Property, new SolidColorBrush(Colors.Transparent)); - + SolidColorBrush finalTarget; var targetVal = control.GetValue(Property); - - // Continue if target prop is not empty & is a SolidColorBrush derivative. - if (typeof(ISolidColorBrush).IsAssignableFrom(targetVal.GetType())) + if (targetVal is null) + { + finalTarget = new SolidColorBrush(Colors.Transparent); + control.SetValue(Property, finalTarget); + } + else if (targetVal is ImmutableSolidColorBrush immutableSolidColorBrush) + { + finalTarget = new SolidColorBrush(immutableSolidColorBrush.Color); + control.SetValue(Property, finalTarget); + } + else if (targetVal is ISolidColorBrush) { - if (_colorAnimator == null) - InitializeColorAnimator(); - - SolidColorBrush finalTarget; - - // If it's ISCB, change it back to SCB. - if (targetVal.GetType() == typeof(ImmutableSolidColorBrush)) - { - var col = (ImmutableSolidColorBrush)targetVal; - targetVal = new SolidColorBrush(col.Color); - control.SetValue(Property, targetVal); - } - finalTarget = targetVal as SolidColorBrush; - - return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete); } + else + { + return Disposable.Empty; + } + + if (_colorAnimator == null) + InitializeColorAnimator(); - return Disposable.Empty; + return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete); } public override SolidColorBrush Interpolate(double p, SolidColorBrush o, SolidColorBrush n) => null; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 5da44c5943..872f69c884 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -394,13 +394,8 @@ namespace Avalonia.Rendering.SceneGraph } } - private static bool ShouldStartLayer(IVisual visual) - { - var o = visual as IAvaloniaObject; - return visual.VisualChildren.Count > 0 && - o != null && - o.IsAnimating(Visual.OpacityProperty); - } + // HACK: Disabled layers because they're broken in current renderer. See #2244. + private static bool ShouldStartLayer(IVisual visual) => false; private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs index 52fd6abb8b..3aa0392496 100644 --- a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs @@ -76,5 +76,7 @@ namespace Avalonia.VisualTree { return !left.Equals(right); } + + public override string ToString() => $"Bounds: {Bounds} Clip: {Clip} Transform {Transform}"; } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 9a12479fac..82cf3c934f 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -207,6 +207,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern void gtk_widget_realize(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern void gtk_widget_destroy(IntPtr gtkWidget); [DllImport(GtkName)] public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); @@ -219,6 +222,13 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GdkName)] static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [DllImport(GdkName)] + public static extern IntPtr gdk_x11_window_get_xid(IntPtr window); + + + [DllImport(GtkName)] + public static extern IntPtr gtk_container_add(IntPtr container, IntPtr widget); [DllImport(GdkName)] static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs new file mode 100644 index 0000000000..add3bc6585 --- /dev/null +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -0,0 +1,190 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using static Avalonia.X11.XLib; +namespace Avalonia.X11 +{ + // TODO: Actually implement XEmbed instead of simply using XReparentWindow + class X11NativeControlHost : INativeControlHostImpl + { + private readonly AvaloniaX11Platform _platform; + public X11Window Window { get; } + + public X11NativeControlHost(AvaloniaX11Platform platform, X11Window window) + { + _platform = platform; + Window = window; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + var ch = new DumbWindow(_platform.Info); + XSync(_platform.Display, false); + return ch; + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(_platform.Info, Window.Handle.Handle); + Attachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Attachment(_platform.Display, holder, _platform.OrphanedWindow, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException(handle.HandleDescriptor + " is not compatible with the current window", + nameof(handle)); + var attachment = new Attachment(_platform.Display, new DumbWindow(_platform.Info, Window.Handle.Handle), + _platform.OrphanedWindow, handle) { AttachedTo = this }; + return attachment; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "XID"; + + class DumbWindow : INativeControlHostDestroyableControlHandle + { + private readonly IntPtr _display; + + public DumbWindow(X11Info x11, IntPtr? parent = null) + { + _display = x11.Display; + /*Handle = XCreateSimpleWindow(x11.Display, XLib.XDefaultRootWindow(_display), + 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);*/ + var attr = new XSetWindowAttributes + { + backing_store = 1, + bit_gravity = Gravity.NorthWestGravity, + win_gravity = Gravity.NorthWestGravity, + + }; + + parent = parent ?? XDefaultRootWindow(x11.Display); + + Handle = XCreateWindow(_display, parent.Value, 0, 0, + 1,1, 0, 0, + (int)CreateWindowArgs.InputOutput, + IntPtr.Zero, + new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | + SetWindowValuemask.BackPixel | + SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => "XID"; + public void Destroy() + { + if (Handle != IntPtr.Zero) + { + XDestroyWindow(_display, Handle); + Handle = IntPtr.Zero; + } + } + } + + class Attachment : INativeControlHostControlTopLevelAttachment + { + private readonly IntPtr _display; + private readonly IntPtr _orphanedWindow; + private DumbWindow _holder; + private IPlatformHandle _child; + private X11NativeControlHost _attachedTo; + private bool _mapped; + + public Attachment(IntPtr display, DumbWindow holder, IntPtr orphanedWindow, IPlatformHandle child) + { + _display = display; + _orphanedWindow = orphanedWindow; + _holder = holder; + _child = child; + XReparentWindow(_display, child.Handle, holder.Handle, 0, 0); + XMapWindow(_display, child.Handle); + } + + public void Dispose() + { + if (_child != null) + { + XReparentWindow(_display, _child.Handle, _orphanedWindow, 0, 0); + _child = null; + } + + _holder?.Destroy(); + _holder = null; + _attachedTo = null; + } + + void CheckDisposed() + { + if (_child == null) + throw new ObjectDisposedException("X11 INativeControlHostControlTopLevelAttachment"); + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (X11NativeControlHost)value; + if (value == null) + { + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + XReparentWindow(_display, _holder.Handle, _orphanedWindow, 0, 0); + } + else + { + XReparentWindow(_display, _holder.Handle, _attachedTo.Window.Handle.Handle, 0, 0); + } + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is X11NativeControlHost; + + public void Hide() + { + if(_attachedTo == null || _child == null) + return; + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + } + + public void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _attachedTo.Window.Scaling); + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + XMoveResizeWindow(_display, _child.Handle, 0, 0, pixelRect.Width, pixelRect.Height); + XMoveResizeWindow(_display, _holder.Handle, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height); + if (!_mapped) + { + XMapWindow(_display, _holder.Handle); + XRaiseWindow(_display, _holder.Handle); + _mapped = true; + } + Console.WriteLine($"Moved {_child.Handle} to {pixelRect}"); + } + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c2b3926ffd..9e9c4fd30e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -26,6 +26,7 @@ namespace Avalonia.X11 public IX11Screens X11Screens { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } + public IntPtr OrphanedWindow { get; private set; } public X11Globals Globals { get; private set; } public void Initialize(X11PlatformOptions options) { @@ -33,6 +34,8 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, + IntPtr.Zero); if (Display == IntPtr.Zero) throw new Exception("XOpenDisplay failed"); XError.Init(); @@ -82,7 +85,7 @@ namespace Avalonia.X11 return new X11Window(this, null); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 90064cb28d..499fe5a67a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -21,7 +21,9 @@ using static Avalonia.X11.XLib; // ReSharper disable StringLiteralTypo namespace Avalonia.X11 { - unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, ITopLevelImplWithNativeMenuExporter + unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client, + ITopLevelImplWithNativeMenuExporter, + ITopLevelImplWithNativeControlHost { private readonly AvaloniaX11Platform _platform; private readonly IWindowImpl _popupParent; @@ -185,6 +187,7 @@ namespace Avalonia.X11 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeControlHost = new X11NativeControlHost(_platform, this); } class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo @@ -313,6 +316,7 @@ namespace Avalonia.X11 public Action Closed { get; set; } public Action PositionChanged { get; set; } + public Action LostFocus { get; set; } public IRenderer CreateRenderer(IRenderRoot root) { @@ -1094,7 +1098,7 @@ namespace Avalonia.X11 public IPopupPositioner PopupPositioner { get; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } - + public INativeControlHostImpl NativeControlHost { get; } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper.SetTransparencyRequest(transparencyLevel); diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index 4e44f55fe0..742973e0da 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -239,13 +239,13 @@ namespace Avalonia.X11 if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated) { - Vector? scrollDelta = ev.Button switch + var scrollDelta = ev.Button switch { 4 => new Vector(0, 1), 5 => new Vector(0, -1), 6 => new Vector(1, 0), 7 => new Vector(-1, 0), - _ => null + _ => (Vector?)null }; if (scrollDelta.HasValue) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index d4a29e39c6..2153344363 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -10,7 +10,7 @@ using Avalonia.Rendering; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; @@ -75,11 +75,7 @@ namespace Avalonia.LinuxFramebuffer public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } - public event Action LostFocus - { - add {} - remove {} - } + public Action LostFocus { get; set; } public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index abace92f08..a8060d3fbf 100644 --- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -24,9 +24,7 @@ namespace Avalonia.Win32.Embedding if (_root.IsFocused) FocusManager.Instance.Focus(null); _root.GotFocus += RootGotFocus; - // ReSharper disable once PossibleNullReferenceException - // Always non-null at this point - _root.PlatformImpl.LostFocus += PlatformImpl_LostFocus; + FixPosition(); } @@ -36,23 +34,6 @@ namespace Avalonia.Win32.Embedding set { _root.Content = value; } } - void Unfocus() - { - var focused = (IVisual)FocusManager.Instance.Current; - if (focused == null) - return; - while (focused.VisualParent != null) - focused = focused.VisualParent; - - if (focused == _root) - KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); - } - - private void PlatformImpl_LostFocus() - { - Unfocus(); - } - protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index b30831667b..f8366abb81 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -18,11 +18,11 @@ using MouseButton = System.Windows.Input.MouseButton; namespace Avalonia.Win32.Interop.Wpf { - class WpfTopLevelImpl : FrameworkElement, IEmbeddableWindowImpl + class WpfTopLevelImpl : FrameworkElement, ITopLevelImpl { private HwndSource _currentHwndSource; private readonly HwndSourceHook _hook; - private readonly IEmbeddableWindowImpl _ttl; + private readonly ITopLevelImpl _ttl; private IInputRoot _inputRoot; private readonly IEnumerable _surfaces; private readonly IMouseDevice _mouse; @@ -241,7 +241,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.TransparencyLevelChanged { get; set; } Action ITopLevelImpl.Closed { get; set; } - public new event Action LostFocus; + public new Action LostFocus { get; set; } internal Vector GetScaling() { diff --git a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs index 34f2894657..e08f1b28be 100644 --- a/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/EmbeddedWindowImpl.cs @@ -6,11 +6,8 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl + class EmbeddedWindowImpl : WindowImpl { - private static IntPtr DefaultParentWindow = CreateParentWindow(); - private static UnmanagedMethods.WndProc _wndProcDelegate; - protected override IntPtr CreateWindowOverride(ushort atom) { var hWnd = UnmanagedMethods.CreateWindowEx( @@ -22,66 +19,12 @@ namespace Avalonia.Win32 0, 640, 480, - DefaultParentWindow, + OffscreenParentWindow.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return hWnd; } - protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) - LostFocus?.Invoke(); - return base.WndProc(hWnd, msg, wParam, lParam); - } - - public event Action LostFocus; - - private static IntPtr CreateParentWindow() - { - _wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); - - var wndClassEx = new UnmanagedMethods.WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = UnmanagedMethods.GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), - }; - - var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - var hwnd = UnmanagedMethods.CreateWindowEx( - 0, - atom, - null, - (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - - return hwnd; - } - - private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); - } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ba3775200b..b3b38db1ab 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32.Interop [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Using Win32 naming for consistency.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements must be documented", Justification = "Look in Win32 docs.")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items must be documented", Justification = "Look in Win32 docs.")] - internal static class UnmanagedMethods + internal unsafe static class UnmanagedMethods { public const int CW_USEDEFAULT = unchecked((int)0x80000000); @@ -1001,6 +1001,10 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, ref RECT lpRect, bool bErase); + + [DllImport("user32.dll")] + public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); + [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1051,6 +1055,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] + public static extern IntPtr GetParent(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); [DllImport("kernel32.dll", SetLastError = true)] diff --git a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs new file mode 100644 index 0000000000..9de105a3d5 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +namespace Avalonia.Win32 +{ + class OffscreenParentWindow + { + public static IntPtr Handle { get; } = CreateParentWindow(); + private static UnmanagedMethods.WndProc s_wndProcDelegate; + private static IntPtr CreateParentWindow() + { + s_wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); + + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + var hwnd = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + + return hwnd; + } + + private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 7f27a9e841..cd25b32ed9 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -10,11 +10,31 @@ namespace Avalonia.Win32 private bool _dropShadowHint = true; private Size? _maxAutoSize; + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + [ThreadStatic] + private static IntPtr s_parentHandle; + public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); + var parent = UnmanagedMethods.GetParent(Handle.Handle); + if (parent != IntPtr.Zero) + { + IntPtr nextParent = parent; + while (nextParent != IntPtr.Zero) + { + parent = nextParent; + nextParent = UnmanagedMethods.GetParent(parent); + } + + UnmanagedMethods.SetFocus(parent); + } } + protected override bool ShouldTakeFocusOnClick => false; + public override Size MaxAutoSizeHint { get @@ -56,10 +76,11 @@ namespace Avalonia.Win32 UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, + s_parentHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + s_parentHandle = IntPtr.Zero; EnableBoxShadow(result, _dropShadowHint); @@ -80,7 +101,22 @@ namespace Avalonia.Win32 } } - public PopupImpl(IWindowBaseImpl parent) + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + static IWindowBaseImpl SaveParentHandle(IWindowBaseImpl parent) + { + s_parentHandle = parent.Handle.Handle; + return parent; + } + + // This is needed because we are calling virtual methods from constructors + // One fabulous design decision leads to another, I guess + public PopupImpl(IWindowBaseImpl parent) : this(SaveParentHandle(parent), false) + { + + } + + private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs new file mode 100644 index 0000000000..69fbe30068 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -0,0 +1,201 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.VisualTree; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class Win32NativeControlHost : INativeControlHostImpl + { + public WindowImpl Window { get; } + + public Win32NativeControlHost(WindowImpl window) + { + Window = window; + } + + void AssertCompatible(IPlatformHandle handle) + { + if (!IsCompatibleWith(handle)) + throw new ArgumentException($"Don't know what to do with {handle.HandleDescriptor}"); + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + AssertCompatible(parent); + return new DumbWindow(parent.Handle); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new DumbWindow(Window.Handle.Handle); + Win32NativeControlAttachment attachment = null; + try + { + var child = create(holder); + // ReSharper disable once UseObjectOrCollectionInitializer + // It has to be assigned to the variable before property setter is called so we dispose it on exception + attachment = new Win32NativeControlAttachment(holder, child); + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + AssertCompatible(handle); + return new Win32NativeControlAttachment(new DumbWindow(Window.Handle.Handle), + handle) { AttachedTo = this }; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "HWND"; + + class DumbWindow : IDisposable, INativeControlHostDestroyableControlHandle + { + public IntPtr Handle { get;} + public string HandleDescriptor => "HWND"; + public void Destroy() => Dispose(); + + UnmanagedMethods.WndProc _wndProcDelegate; + private readonly string _className; + + public DumbWindow(IntPtr? parent = null) + { + _wndProcDelegate = WndProc; + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = _wndProcDelegate, + lpszClassName = _className = "AvaloniaDumbWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + Handle = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_CHILD, + 0, + 0, + 640, + 480, + parent ?? OffscreenParentWindow.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + } + + + + protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + private void ReleaseUnmanagedResources() + { + UnmanagedMethods.DestroyWindow(Handle); + UnmanagedMethods.UnregisterClass(_className, UnmanagedMethods.GetModuleHandle(null)); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~DumbWindow() + { + ReleaseUnmanagedResources(); + } + } + + class Win32NativeControlAttachment : INativeControlHostControlTopLevelAttachment + { + private DumbWindow _holder; + private IPlatformHandle _child; + private Win32NativeControlHost _attachedTo; + + public Win32NativeControlAttachment(DumbWindow holder, IPlatformHandle child) + { + _holder = holder; + _child = child; + UnmanagedMethods.SetParent(child.Handle, _holder.Handle); + UnmanagedMethods.ShowWindow(child.Handle, UnmanagedMethods.ShowWindowCommand.Show); + } + + void CheckDisposed() + { + if (_holder == null) + throw new ObjectDisposedException(nameof(Win32NativeControlAttachment)); + } + + public void Dispose() + { + if (_child != null) + UnmanagedMethods.SetParent(_child.Handle, OffscreenParentWindow.Handle); + _holder?.Dispose(); + _holder = null; + _child = null; + _attachedTo = null; + } + + public INativeControlHostImpl AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + _attachedTo = (Win32NativeControlHost) value; + if (_attachedTo == null) + { + UnmanagedMethods.ShowWindow(_holder.Handle, UnmanagedMethods.ShowWindowCommand.Hide); + UnmanagedMethods.SetParent(_holder.Handle, OffscreenParentWindow.Handle); + } + else + UnmanagedMethods.SetParent(_holder.Handle, _attachedTo.Window.Handle.Handle); + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is Win32NativeControlHost; + + public void Hide() + { + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, + -100, -100, 1, 1, + UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | + UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + } + + public unsafe void ShowInBounds(TransformedBounds transformedBounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform) * + new Vector(_attachedTo.Window.Scaling, _attachedTo.Window.Scaling); + var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), + Math.Max(1, (int)bounds.Height)); + + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, pixelRect.Width, pixelRect.Height, true); + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, pixelRect.X, pixelRect.Y, pixelRect.Width, + pixelRect.Height, + UnmanagedMethods.SetWindowPosFlags.SWP_SHOWWINDOW + | UnmanagedMethods.SetWindowPosFlags.SWP_NOZORDER + | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + + UnmanagedMethods.InvalidateRect(_attachedTo.Window.Handle.Handle, null, false); + } + } + + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e324febfd5..b7bb0e19ba 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -209,7 +209,7 @@ namespace Avalonia.Win32 return new WindowImpl(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { var embedded = new EmbeddedWindowImpl(); embedded.Show(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 50e71aeebe..391abdfc73 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Win32.Input; @@ -22,7 +23,8 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; - + var shouldTakeFocus = false; + switch ((WindowsMessage)msg) { case WindowsMessage.WM_ACTIVATE: @@ -149,6 +151,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { + shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -177,6 +180,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { + shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -429,12 +433,18 @@ namespace Avalonia.Win32 (Screen as ScreenImpl)?.InvalidateScreensCache(); return IntPtr.Zero; } + case WindowsMessage.WM_KILLFOCUS: + LostFocus?.Invoke(); + break; } #if USE_MANAGED_DRAG if (_managedDrag.PreprocessInputEvent(ref e)) return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); #endif + + if (shouldTakeFocus) + SetFocus(_hwnd); if (e != null && Input != null) { @@ -519,5 +529,9 @@ namespace Avalonia.Win32 return modifiers; } + + public INativeControlHostImpl NativeControlHost => _nativeControlHost; + + protected virtual bool ShouldTakeFocusOnClick => true; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6024dc01da..36398eb810 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; @@ -18,7 +19,8 @@ namespace Avalonia.Win32 /// /// Window implementation for Win32 platform. /// - public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, + ITopLevelImplWithNativeControlHost { private static readonly List s_instances = new List(); @@ -52,6 +54,7 @@ namespace Avalonia.Win32 private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; + private Win32NativeControlHost _nativeControlHost; private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; @@ -101,6 +104,7 @@ namespace Avalonia.Win32 Screen = new ScreenImpl(); + _nativeControlHost = new Win32NativeControlHost(this); s_instances.Add(this); } @@ -123,6 +127,8 @@ namespace Avalonia.Win32 public Action PositionChanged { get; set; } public Action WindowStateChanged { get; set; } + + public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } @@ -523,7 +529,7 @@ namespace Avalonia.Win32 0, atom, null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, + (int)WindowStyles.WS_OVERLAPPEDWINDOW | (int) WindowStyles.WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 838bf49846..d299ff99c1 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.iOS { - class EmbeddableImpl : TopLevelImpl, IEmbeddableWindowImpl + class EmbeddableImpl : TopLevelImpl { public void SetTitle(string title) { @@ -23,11 +23,5 @@ namespace Avalonia.iOS public void SetSystemDecorations(SystemDecorations enabled) { } - - public event Action LostFocus - { - add {} - remove {} - } } } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index c5b6642982..83a68990d7 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -139,5 +139,7 @@ namespace Avalonia.iOS public ILockedFramebuffer Lock() => new EmulatedFramebuffer(this); public IPopupImpl CreatePopup() => null; + + public Action LostFocus { get; set; } } } diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs index 9a40b7162e..aaca5f53d6 100644 --- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs +++ b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.iOS throw new NotSupportedException(); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public WindowImpl CreateEmbeddableWindow() { throw new NotSupportedException(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index df871a67b4..784046ac0b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -578,6 +578,28 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.True(true); } + [Fact] + public void Should_Not_Throw_Exception_When_Enabling_Data_Validation_On_Missing_Member() + { + var source = new Class1(); + var target = new PropertyAccessorNode("NotFound", true); + + target.Target = new WeakReference(source); + + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + Assert.Equal( + new object[] + { + new BindingNotification( + new MissingMemberException("Could not find a matching property accessor for 'NotFound' on 'Avalonia.Base.UnitTests.Data.Core.ExpressionObserverTests_Property+Class1'"), + BindingErrorType.Error), + }, + result); + } + private interface INext { int PropertyChangedSubscriptionCount { get; } diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index de40c247e6..7170f6d7d4 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -24,7 +24,7 @@ namespace Avalonia.Benchmarks.Layout Renderer = new NullRenderer() }; - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark] diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index d03d17b4d3..fce2cddec9 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -25,7 +25,7 @@ namespace Avalonia.Benchmarks.Layout _controls.Add(panel); _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 5); - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs index fc2380d670..c6da3a941f 100644 --- a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -26,7 +26,7 @@ namespace Avalonia.Benchmarks.Traversal _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); - _root.LayoutManager.ExecuteInitialLayoutPass(_root); + _root.LayoutManager.ExecuteInitialLayoutPass(); } [Benchmark] diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index b3882c534b..4ffa526b85 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1194,7 +1194,7 @@ namespace Avalonia.Controls.UnitTests Height = 50, }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); PrintColumnDefinitions(grids[0]); Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index a7679ba388..c4346e571b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -329,7 +329,7 @@ namespace Avalonia.Controls.UnitTests return tb; }, true); - lm.ExecuteInitialLayoutPass(wnd); + lm.ExecuteInitialLayoutPass(); target.Items = items; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 9caae89cfe..d529cc4f75 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -232,7 +232,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroll = (TestScroller)target.Parent; scroll.Width = scroll.Height = 100; - scroll.LayoutManager.ExecuteInitialLayoutPass(scroll); + scroll.LayoutManager.ExecuteInitialLayoutPass(); // Ensure than an intermediate measure pass doesn't add more controls than it // should. This can happen if target gets measured with Size.Infinity which @@ -324,6 +324,11 @@ namespace Avalonia.Controls.UnitTests.Presenters private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { + public TestScroller() + { + LayoutManager = new LayoutManager(this); + } + public IRenderer Renderer { get; } public Size ClientSize { get; } public double RenderScaling => 1; @@ -332,7 +337,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; } public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); public void Invalidate(Rect rect) => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 5a2cb60a56..a467c6dd03 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -723,7 +723,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var last = (target.Items as IList)[10]; @@ -740,7 +740,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var last = (target.Items as IList)[10]; @@ -838,7 +838,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -855,7 +855,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -874,7 +874,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 100; scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -893,7 +893,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 100; scroller.Height = 95; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); ((ILogicalScrollable)target).Offset = new Vector(0, 11); var from = target.Panel.Children[1]; @@ -946,7 +946,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -963,7 +963,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = (TestScroller)target.Parent; scroller.Width = scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -982,7 +982,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 95; scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -1001,7 +1001,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Width = 95; scroller.Height = 100; - scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + scroller.LayoutManager.ExecuteInitialLayoutPass(); ((ILogicalScrollable)target).Offset = new Vector(11, 0); var from = target.Panel.Children[1]; @@ -1062,6 +1062,11 @@ namespace Avalonia.Controls.UnitTests.Presenters private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, ILogicalRoot { + public TestScroller() + { + LayoutManager = new LayoutManager(this); + } + public IRenderer Renderer { get; } public Size ClientSize { get; } public double RenderScaling => 1; @@ -1070,7 +1075,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; } public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); public void Invalidate(Rect rect) => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index deca3cfb75..ab21c5d330 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -158,7 +158,7 @@ namespace Avalonia.Controls.UnitTests target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); target.ScrollChanged += (s, e) => { @@ -188,7 +188,7 @@ namespace Avalonia.Controls.UnitTests target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); target.ScrollChanged += (s, e) => { @@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); target.Offset = new Vector(10, 10); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); target.ScrollChanged += (s, e) => { diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 6b107b0187..e49e273bec 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests } }; - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds); } @@ -267,6 +267,23 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Dispose_LayoutManager() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + + var layoutManager = new Mock(); + var target = new TestTopLevel(impl.Object, layoutManager.Object); + + impl.Object.Closed(); + + layoutManager.Verify(x => x.Dispose()); + } + } + [Fact] public void Reacts_To_Changes_In_Global_Styles() { @@ -282,7 +299,7 @@ namespace Avalonia.Controls.UnitTests Content = child, }; - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Thickness(0), child.BorderThickness); @@ -295,7 +312,7 @@ namespace Avalonia.Controls.UnitTests }; Application.Current.Styles.Add(style); - target.LayoutManager.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Thickness(2), child.BorderThickness); @@ -323,7 +340,7 @@ namespace Avalonia.Controls.UnitTests public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null) : base(impl) { - _layoutManager = layoutManager ?? new LayoutManager(); + _layoutManager = layoutManager ?? new LayoutManager(this); } protected override ILayoutManager CreateLayoutManager() => _layoutManager; diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index a9c991b8b9..25e8c82b1a 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -20,11 +20,11 @@ namespace Avalonia.Controls.UnitTests return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 01f0ec247f..1efbbed2e8 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -185,6 +185,35 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Non_TabStop_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + (current = new Button { Name = "Button3" }), + new Button { Name="Button4", [KeyboardNavigation.IsTabStopProperty] = false } + } + }, + (next = new Button { Name = "Button5" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Returns_First_Control_In_Next_Uncle_Container() { diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 332e8a751d..e429adce85 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateMeasure(); @@ -23,13 +23,29 @@ namespace Avalonia.Layout.UnitTests Assert.True(control.Arranged); } + [Fact] + public void Doesnt_Measure_And_Arrange_InvalidateMeasured_Control_When_TopLevel_Is_Not_Visible() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control, IsVisible = false }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + [Fact] public void Arranges_InvalidateArranged_Control() { var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateArrange(); @@ -45,7 +61,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot(); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Child = control; root.Measured = root.Arranged = false; @@ -80,7 +96,7 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control2.InvalidateMeasure(); control1.InvalidateMeasure(); @@ -115,7 +131,7 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control2.InvalidateMeasure(); root.InvalidateMeasure(); @@ -132,7 +148,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measured = root.Arranged = false; control.Measured = control.Arranged = false; @@ -151,7 +167,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = control.Arranged = false; control.InvalidateMeasure(); @@ -177,7 +193,7 @@ namespace Avalonia.Layout.UnitTests return new Size(100, 100); }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(Size.Infinity, availableSize); } @@ -199,7 +215,7 @@ namespace Avalonia.Layout.UnitTests return s; }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(100, 100), arrangeSize); root.Width = 120; @@ -225,7 +241,7 @@ namespace Avalonia.Layout.UnitTests } }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; @@ -241,7 +257,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; int cnt = 0; @@ -272,7 +288,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Arranged = false; int cnt = 0; @@ -313,7 +329,7 @@ namespace Avalonia.Layout.UnitTests panel.Children.AddRange(nonArrageableTargets); panel.Children.AddRange(targets); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); foreach (var c in panel.Children.OfType()) { @@ -347,7 +363,7 @@ namespace Avalonia.Layout.UnitTests var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; control.DoMeasureOverride = (l, s) => @@ -380,7 +396,7 @@ namespace Avalonia.Layout.UnitTests var root = new LayoutTestRoot { Child = control }; var count = 0; - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); control.Measured = false; control.DoMeasureOverride = (l, s) => @@ -399,7 +415,7 @@ namespace Avalonia.Layout.UnitTests root.InvalidateMeasure(); control.InvalidateMeasure(); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(new Size(200, 200), control.Bounds.Size); Assert.Equal(new Size(200, 200), control.DesiredSize); diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs index a21c8d589d..44a5af94b9 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -208,14 +208,12 @@ namespace Avalonia.Layout.UnitTests { Border border1; Border border2; - var layoutManager = new LayoutManager(); var root = new TestRoot { Child = border1 = new Border { Child = border2 = new Border(), }, - LayoutManager = layoutManager, }; var raised = 0; @@ -233,7 +231,7 @@ namespace Avalonia.Layout.UnitTests root.Measure(new Size(100, 100)); root.Arrange(new Rect(0, 0, 100, 100)); - layoutManager.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(3, raised); Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds); diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs new file mode 100644 index 0000000000..504e3fa585 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs @@ -0,0 +1,424 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests_EffectiveViewportChanged + { + [Fact] + public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; + + root.Child = target; + + Assert.Equal(0, raised); + }); + } + + [Fact] + public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas(); + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + await ExecuteInitialLayoutPass(root); + }); + } + + [Fact] + public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new TestCanvas(); + var raised = 0; + var layoutUpdatedRaised = 0; + + root.LayoutUpdated += (s, e) => + { + Assert.Equal(2, target.MeasureCount); + Assert.Equal(2, target.ArrangeCount); + ++layoutUpdatedRaised; + }; + + target.EffectiveViewportChanged += (s, e) => + { + target.InvalidateMeasure(); + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + + Assert.Equal(1, raised); + Assert.Equal(1, layoutUpdatedRaised); + }); + } + + [Fact] + public async Task Viewport_Extends_Beyond_Centered_Control() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52, }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + root.Child = target; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Viewport_Extends_Beyond_Nested_Centered_Control() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 52, Height = 52 }; + var parent = new Border { Width = 100, Height = 100, Child = target }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task ScrollViewer_Determines_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport); + ++raised; + }; + + root.Child = scroller; + + await ExecuteInitialLayoutPass(root); + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 200, Height = 200 }; + var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate() }; + var raised = 0; + + root.Child = scroller; + + await ExecuteInitialLayoutPass(root); + scroller.Offset = new Vector(0, 10); + + await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) => + { + Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport); + ++raised; + }); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Moving_Parent_Updates_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + parent.Margin = new Thickness(8, 0, 0, 0); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + target.EffectiveViewportChanged += (s, e) => ++raised; + target.RenderTransform = new TranslateTransform { X = 8 }; + target.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(0, raised); + }); + } + + [Fact] + public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport); + ++raised; + }; + + // Change the parent render transform to move it. A layout is then needed before + // EffectiveViewportChanged is raised. + parent.RenderTransform = new TranslateTransform { X = 8 }; + parent.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + [Fact] + public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport() + { + await RunOnUIThread.Execute(async () => + { + var root = CreateRoot(); + var target = new Canvas { Width = 100, Height = 100 }; + var parent = new Border { Width = 200, Height = 200, Child = target }; + var raised = 0; + + root.Child = parent; + + await ExecuteInitialLayoutPass(root); + + target.EffectiveViewportChanged += (s, e) => + { + AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport); + ++raised; + }; + + parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + parent.RenderTransform = new RotateTransform { Angle = 45 }; + parent.InvalidateMeasure(); + await ExecuteLayoutPass(root); + + Assert.Equal(1, raised); + }); + } + + private TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 }; + + private Task ExecuteInitialLayoutPass(TestRoot root) + { + root.LayoutManager.ExecuteInitialLayoutPass(); + return Task.CompletedTask; + } + + private Task ExecuteLayoutPass(TestRoot root) + { + root.LayoutManager.ExecuteLayoutPass(); + return Task.CompletedTask; + } + + private Task ExecuteScrollerLayoutPass( + TestRoot root, + ScrollViewer scroller, + Control target, + Action handler) + { + void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e) + { + handler(sender, e); + } + + target.EffectiveViewportChanged += ViewportChanged; + root.LayoutManager.ExecuteLayoutPass(); + return Task.CompletedTask; + } + private IControlTemplate ScrollViewerTemplate() + { + return new FuncControlTemplate((control, scope) => new Grid + { + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(GridLength.Auto), + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + }, + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], + [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], + [~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "horizontalScrollBar", + Orientation = Orientation.Horizontal, + [~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], + [~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], + [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], + [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], + [Grid.RowProperty] = 1, + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + Orientation = Orientation.Vertical, + [~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], + [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], + [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], + [Grid.ColumnProperty] = 1, + }.RegisterInNameScope(scope), + }, + }); + } + + private void AssertArePixelEqual(Rect expected, Rect actual) + { + var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height); + var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height); + Assert.Equal(expectedRounded, actualRounded); + } + + private class TestCanvas : Canvas + { + public int MeasureCount { get; private set; } + public int ArrangeCount { get; private set; } + + protected override Size MeasureOverride(Size availableSize) + { + ++MeasureCount; + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + ++ArrangeCount; + return base.ArrangeOverride(finalSize); + } + } + + private static class RunOnUIThread + { + public static async Task Execute(Func func) + { + await func(); + } + } + } +} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 9bb9fd7145..00ef503b8d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -44,7 +44,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. @@ -82,7 +82,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Find("foo")); Assert.IsType(window.Presenter.Child); @@ -122,7 +122,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); @@ -159,7 +159,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.NotEmpty(window.Presenter.Child.GetVisualChildren()); @@ -203,7 +203,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); @@ -241,7 +241,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Same(textBox, window.Presenter.Child); // Get the border from the TextBox template. @@ -295,7 +295,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that TreeViewItems get realized. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Single(target.ItemContainerGenerator.Containers); // Clear the content and ensure the TreeView is removed. @@ -329,7 +329,7 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Slider gets added to visual tree. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Slider is removed. @@ -403,7 +403,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that Canvas gets added to visual tree with // its render transform. - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); var canvas = Assert.IsType(window.Presenter.Child); Assert.IsType(canvas.RenderTransform); @@ -451,6 +451,7 @@ namespace Avalonia.LeakTests AttachShowAndDetachContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -486,6 +487,7 @@ namespace Avalonia.LeakTests BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); + Mock.Get(window.PlatformImpl).ResetCalls(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -512,7 +514,7 @@ namespace Avalonia.LeakTests window.Show(); - window.LayoutManager.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); window.Content = null; diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index ee45433089..48a333dc54 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -118,7 +118,7 @@ namespace Avalonia.UnitTests } } - public IEmbeddableWindowImpl CreateEmbeddableWindow() + public IWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index f291d386aa..b6f3a020e8 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -19,6 +19,8 @@ namespace Avalonia.UnitTests public TestRoot() { Renderer = Mock.Of(); + LayoutManager = new LayoutManager(this); + IsVisible = true; } public TestRoot(IControl child) @@ -44,7 +46,7 @@ namespace Avalonia.UnitTests public double LayoutScaling { get; set; } = 1; - public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; set; } public double RenderScaling => 1; diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index da4d92ce5e..38ab3c3c5d 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -16,6 +16,7 @@ namespace Avalonia.UnitTests public TestTemplatedRoot() { + LayoutManager = new LayoutManager(this); Template = new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", @@ -28,7 +29,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); + public ILayoutManager LayoutManager { get; set; } public double RenderScaling => 1; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 767111b89b..bfcc341eed 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -557,7 +557,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -599,7 +599,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -629,7 +629,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -658,7 +658,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Should_Draw_Transparent_Layer_With_Correct_Opacity() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs index 52552f0bee..acee9a50f5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs @@ -134,7 +134,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(child); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measure(new Size(50, 100)); root.Arrange(new Rect(new Size(50, 100))); @@ -171,7 +171,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(child); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); root.Measure(new Size(300, 100)); root.Arrange(new Rect(new Size(300, 100))); @@ -222,7 +222,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(rootGrid); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); root.Measure(rootSize); @@ -277,7 +277,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering var root = new TestRoot(rootGrid); root.Renderer = new ImmediateRenderer(root); - root.LayoutManager.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); root.Measure(rootSize); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index b0f890b484..e219682fa6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -653,7 +653,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -696,7 +696,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -744,7 +744,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); @@ -810,7 +810,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; Assert.Equal(expected, scene.Layers[tree].Dirty.ToArray()); - Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); + + // Layers are disabled. See #2244 + // Assert.Equal(expected, scene.Layers[border].Dirty.ToArray()); } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index 1bece3ae22..19b4c7e606 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -14,7 +14,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { public partial class SceneBuilderTests { - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -40,7 +40,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -105,7 +105,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -118,7 +118,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -147,7 +147,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -168,7 +168,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void Hiding_Transparent_Control_Should_Remove_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -197,7 +197,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); @@ -218,7 +218,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } - [Fact] + [Fact(Skip = "Layers are disabled. See #2244")] public void GeometryClip_Should_Affect_Child_Layers() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) @@ -241,7 +241,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph }; var layout = tree.LayoutManager; - layout.ExecuteInitialLayoutPass(tree); + layout.ExecuteInitialLayoutPass(); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);