diff --git a/.ncrunch/NativeEmbedSample.v3.ncrunchproject b/.ncrunch/NativeEmbedSample.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/NativeEmbedSample.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 5bff2fa0a0..f6dc039c2f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -203,14 +203,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src 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}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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 Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 - src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5 src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 @@ -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..6800ff7d68 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; @@ -276,6 +278,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; + virtual HRESULT TakeFocusFromChildren() = 0; }; AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown @@ -295,6 +298,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 +482,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 ShowInBounds(float x, float y, float width, float height) = 0; + virtual void HideWithSize(float width, float height) = 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..5ee2344ac7 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -0,0 +1,160 @@ +#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 ShowInBounds(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->ShowInBounds(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 HideWithSize(float width, float height) override + { + if(_child == nil) + return; + if(AvnInsidePotentialDeadlock::IsInside()) + { + IAvnNativeControlHostTopLevelAttachment* slf = this; + slf->AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + slf->HideWithSize(width, height); + slf->Release(); + }); + return; + } + + NSRect frame = {0, 0, width, height}; + [_holder setHidden: true]; + [_child setFrame: frame]; + } + + 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..7f8a6e1393 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -116,10 +116,15 @@ public: { SetPosition(lastPositionSet); UpdateStyle(); - - [Window makeKeyAndOrderFront:Window]; - [NSApp activateIgnoringOtherApps:YES]; - + if(ShouldTakeFocusOnShow()) + { + [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; + } + else + { + [Window orderFront: Window]; + } [Window setTitle:_lastTitle]; _shown = true; @@ -128,6 +133,11 @@ public: } } + virtual bool ShouldTakeFocusOnShow() + { + return true; + } + virtual HRESULT Hide () override { @autoreleasepool @@ -390,6 +400,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]; @@ -766,6 +784,15 @@ private: } } + virtual HRESULT TakeFocusFromChildren () override + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + return S_OK; + } + void EnterFullScreenMode () { _fullScreenActive = true; @@ -1060,9 +1087,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 +1169,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 +1195,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 +1214,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]; @@ -1836,7 +1877,6 @@ private: WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; } - protected: virtual NSWindowStyleMask GetStyle() override { @@ -1854,6 +1894,11 @@ protected: return S_OK; } } +public: + virtual bool ShouldTakeFocusOnShow() override + { + return false; + } }; extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index d6e8889a12..fe877dc49c 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/MainView.xaml b/samples/ControlCatalog/MainView.xaml index fef68bb5f5..681c7747c9 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -52,7 +52,9 @@ + + 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/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 028a294492..e600e644af 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -16,6 +16,8 @@ + + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 6fa9fc515e..cce80a2d3c 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,23 +5,32 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { public class ItemsRepeaterPage : UserControl { + private readonly ItemsRepeaterPageViewModel _viewModel; private ItemsRepeater _repeater; private ScrollViewer _scroller; + private Button _scrollToLast; + private Button _scrollToRandom; + private Random _random = new Random(0); public ItemsRepeaterPage() { this.InitializeComponent(); _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); + _scrollToLast = this.FindControl + + + + + Text + + + Tooltip + + + + + + + + + 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.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index 5b0dea6c60..e006459652 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; namespace Avalonia.Animation.Easings @@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings /// Returns the instance of the parsed type. public static Easing Parse(string e) { + if (e.Contains(',')) + { + return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); + } + if (_easingTypes == null) { _easingTypes = new Dictionary(); diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs new file mode 100644 index 0000000000..975fcc4746 --- /dev/null +++ b/src/Avalonia.Animation/Easing/SplineEasing.cs @@ -0,0 +1,85 @@ +namespace Avalonia.Animation.Easings +{ + /// + /// Eases a value + /// using a user-defined cubic bezier curve. + /// Good for custom easing functions that doesn't quite + /// fit with the built-in ones. + /// + public class SplineEasing : Easing + { + /// + /// X coordinate of the first control point + /// + public double X1 + { + get => _internalKeySpline.ControlPointX1; + set + { + _internalKeySpline.ControlPointX1 = value; + } + } + + /// + /// Y coordinate of the first control point + /// + public double Y1 + { + get => _internalKeySpline.ControlPointY1; + set + { + _internalKeySpline.ControlPointY1 = value; + } + } + + /// + /// X coordinate of the second control point + /// + public double X2 + { + get => _internalKeySpline.ControlPointX2; + set + { + _internalKeySpline.ControlPointX2 = value; + } + } + + /// + /// Y coordinate of the second control point + /// + public double Y2 + { + get => _internalKeySpline.ControlPointY2; + set + { + _internalKeySpline.ControlPointY2 = value; + } + } + + private readonly KeySpline _internalKeySpline; + + public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d) + { + _internalKeySpline = new KeySpline(); + + this.X1 = x1; + this.Y1 = y1; + this.X2 = x2; + this.Y1 = y2; + } + + public SplineEasing(KeySpline keySpline) + { + _internalKeySpline = keySpline; + } + + public SplineEasing() + { + _internalKeySpline = new KeySpline(); + } + + /// + public override double Ease(double progress) => + _internalKeySpline.GetSplineProgress(progress); + } +} diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index 5a4f7a15a3..a6e9769186 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -81,7 +81,10 @@ namespace Avalonia.Animation /// A with the appropriate values set public static KeySpline Parse(string value, CultureInfo culture) { - using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + if (culture is null) + culture = CultureInfo.InvariantCulture; + + using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) { return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } @@ -98,6 +101,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX1 = value; + _isDirty = true; } else { @@ -112,7 +116,11 @@ namespace Avalonia.Animation public double ControlPointY1 { get => _controlPointY1; - set => _controlPointY1 = value; + set + { + _controlPointY1 = value; + _isDirty = true; + } } /// @@ -126,6 +134,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX2 = value; + _isDirty = true; } else { @@ -140,7 +149,11 @@ namespace Avalonia.Animation public double ControlPointY2 { get => _controlPointY2; - set => _controlPointY2 = value; + set + { + _controlPointY2 = value; + _isDirty = true; + } } /// @@ -330,20 +343,4 @@ namespace Avalonia.Animation } } } - - /// - /// Converts string values to values - /// - public class KeySplineTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return KeySpline.Parse((string)value, culture); - } - } } diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs new file mode 100644 index 0000000000..cd7427a37d --- /dev/null +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +// Ported from WPF open-source code. +// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + /// + /// Converts string values to values + /// + public class KeySplineTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return KeySpline.Parse((string)value, culture); + } + } +} diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index d915887e4c..b52829a60f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -140,6 +140,7 @@ namespace Avalonia.Collections } } + [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")] public static IAvaloniaReadOnlyList CreateDerivedList( this IAvaloniaReadOnlyList collection, Func select) 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.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 06a1cd4ae5..2a92e75f58 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -206,39 +206,6 @@ namespace Avalonia.Utilities } } - /// - /// Calculates the value to be used for layout rounding at high DPI. - /// - /// Input value to be rounded. - /// Ratio of screen's DPI to layout DPI - /// Adjusted value that will produce layout rounding on screen at high dpi. - /// This is a layout helper method. It takes DPI into account and also does not return - /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with - /// UseLayoutRounding property and should not be used as a general rounding utility. - public static double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.IsOne(dpiScale)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - /// /// Clamps a value between a minimum and maximum value. /// 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/Grid.cs b/src/Avalonia.Controls/Grid.cs index e10d78917e..6357ec98a8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -8,10 +8,8 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using System.Threading; -using Avalonia; -using Avalonia.Collections; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -2103,7 +2101,7 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Count; ++i) { DefinitionBase def = definitions[i]; - double roundedSize = MathUtilities.RoundLayoutValue(def.SizeCache, dpi); + double roundedSize = LayoutHelper.RoundLayoutValue(def.SizeCache, dpi); roundingErrors[i] = (roundedSize - def.SizeCache); def.SizeCache = roundedSize; roundedTakenSize += roundedSize; diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs index 6b5cb2ee25..93f3a0abb8 100644 --- a/src/Avalonia.Controls/IScrollAnchorProvider.cs +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -1,9 +1,29 @@ namespace Avalonia.Controls { + /// + /// Specifies a contract for a scrolling control that supports scroll anchoring. + /// public interface IScrollAnchorProvider { + /// + /// The currently chosen anchor element to use for scroll anchoring. + /// IControl CurrentAnchor { get; } + + /// + /// Registers a control as a potential scroll anchor candidate. + /// + /// + /// A control within the subtree of the . + /// void RegisterAnchorCandidate(IControl element); + + /// + /// Unregisters a control as a potential scroll anchor candidate. + /// + /// + /// A control within the subtree of the . + /// void UnregisterAnchorCandidate(IControl element); } } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs new file mode 100644 index 0000000000..20eac11c2c --- /dev/null +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + public class NativeControlHost : Control + { + private TopLevel _currentRoot; + private INativeControlHostImpl _currentHost; + private INativeControlHostControlTopLevelAttachment _attachment; + private IPlatformHandle _nativeControlHandle; + private bool _queuedForDestruction; + private bool _queuedForMoveResize; + private readonly List _propertyChangedSubscriptions = new List(); + private readonly EventHandler _propertyChangedHandler; + static NativeControlHost() + { + IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); + } + + public NativeControlHost() + { + _propertyChangedHandler = PropertyChangedHandler; + } + + private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) + => host.UpdateHost(); + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = e.Root as TopLevel; + var visual = (IVisual)this; + while (visual != _currentRoot) + { + + if (visual is Visual v) + { + v.PropertyChanged += _propertyChangedHandler; + _propertyChangedSubscriptions.Add(v); + } + + visual = visual.GetVisualParent(); + } + + UpdateHost(); + } + + private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.IsEffectiveValueChange && e.Property == BoundsProperty) + EnqueueForMoveResize(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _currentRoot = null; + if (_propertyChangedSubscriptions != null) + { + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= _propertyChangedHandler; + _propertyChangedSubscriptions.Clear(); + } + UpdateHost(); + } + + + private void UpdateHost() + { + _queuedForMoveResize = false; + _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; + var needsAttachment = _currentHost != null; + + 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 (_attachment?.AttachedTo != _currentHost) + return; + + TryUpdateNativeControlPosition(); + } + + + private Rect? GetAbsoluteBounds() + { + var bounds = Bounds; + var position = this.TranslatePoint(bounds.Position, _currentRoot); + if (position == null) + return null; + return new Rect(position.Value, bounds.Size); + } + + void EnqueueForMoveResize() + { + if(_queuedForMoveResize) + return; + _queuedForMoveResize = true; + Dispatcher.UIThread.Post(UpdateHost, DispatcherPriority.Render); + } + + public bool TryUpdateNativeControlPosition() + { + if (_currentHost == null) + return false; + + var bounds = GetAbsoluteBounds(); + var needsShow = IsEffectivelyVisible && bounds.HasValue; + + if (needsShow) + _attachment?.ShowInBounds(bounds.Value); + else + _attachment?.HideWithSize(Bounds.Size); + return false; + } + + private void CheckDestruction() + { + _queuedForDestruction = false; + if (_currentRoot == null) + DestroyNativeControl(); + } + + protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + if (_currentHost == null) + throw new InvalidOperationException(); + return _currentHost.CreateDefaultChild(parent); + } + + private 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..c6b1d09849 --- /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 HideWithSize(Size size); + void ShowInBounds(Rect rect); + } + + 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/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 98a5b10023..327cfb7736 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Runtime.InteropServices.ComTypes; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -14,7 +12,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents a scrolling view of content inside a . /// - public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable + public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -58,13 +56,19 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); + // Arbitrary chosen value, probably need to ask ILogicalScrollable + private const int LogicalScrollItemSize = 50; + private bool _canHorizontallyScroll; private bool _canVerticallyScroll; + private bool _arranging; private Size _extent; private Vector _offset; private IDisposable _logicalScrollSubscription; private Size _viewport; private Dictionary _activeLogicalGestureScrolls; + private List _anchorCandidates; + private (IControl control, Rect bounds) _anchor; /// /// Initializes static members of the class. @@ -73,7 +77,6 @@ namespace Avalonia.Controls.Presenters { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - AffectsArrange(OffsetProperty); } /// @@ -87,6 +90,8 @@ namespace Avalonia.Controls.Presenters this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } + internal event EventHandler PreArrange; + /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// @@ -120,7 +125,7 @@ namespace Avalonia.Controls.Presenters public Vector Offset { get { return _offset; } - set { SetAndRaise(OffsetProperty, ref _offset, value); } + set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); } } /// @@ -132,6 +137,9 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; + /// /// Attempts to bring a portion of the target visual into view by scrolling the content. /// @@ -196,6 +204,30 @@ namespace Avalonia.Controls.Presenters return result; } + /// + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + if (!this.IsVisualAncestorOf(element)) + { + throw new InvalidOperationException( + "An anchor control must be a visual descendent of the ScrollContentPresenter."); + } + + _anchorCandidates ??= new List(); + _anchorCandidates.Add(element); + } + + /// + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + _anchorCandidates?.Remove(element); + + if (_anchor.control == element) + { + _anchor = default; + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -215,22 +247,85 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { + PreArrange?.Invoke(this, new VectorEventArgs + { + Vector = new Vector(finalSize.Width, finalSize.Height), + }); + if (_logicalScrollSubscription != null || Child == null) { return base.ArrangeOverride(finalSize); } + try + { + _arranging = true; + return ArrangeWithAnchoring(finalSize); + } + finally + { + _arranging = false; + } + } + + private Size ArrangeWithAnchoring(Size finalSize) + { var size = new Size( CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width, CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height); + + Vector TrackAnchor() + { + // If we have an anchor and its position relative to Child has changed during the + // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust + // relative positions within Child). + if (_anchor.control != null && + TranslateBounds(_anchor.control, Child, out var updatedBounds) && + updatedBounds.Position != _anchor.bounds.Position) + { + var offset = updatedBounds.Position - _anchor.bounds.Position; + return offset; + } + + return default; + } + + // Calculate the new anchor element. + _anchor = CalculateCurrentAnchor(); + + // Do the arrange. ArrangeOverrideImpl(size, -Offset); + + // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. + var anchorShift = TrackAnchor(); + + if (anchorShift != default) + { + var newOffset = Offset + anchorShift; + var newExtent = Extent; + var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); + + if (newOffset.X > maxOffset.X) + { + newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); + } + + if (newOffset.Y > maxOffset.Y) + { + newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); + } + + Extent = newExtent; + Offset = newOffset; + ArrangeOverrideImpl(size, -Offset); + } + Viewport = finalSize; Extent = Child.Bounds.Size.Inflate(Child.Margin); + return finalSize; } - // Arbitrary chosen value, probably need to ask ILogicalScrollable - private const int LogicalScrollItemSize = 50; private void OnScrollGesture(object sender, ScrollGestureEventArgs e) { if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) @@ -327,6 +422,16 @@ namespace Avalonia.Controls.Presenters } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == OffsetProperty && !_arranging) + { + InvalidateArrange(); + } + + base.OnPropertyChanged(change); + } + private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e) { e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect); @@ -390,5 +495,84 @@ namespace Avalonia.Controls.Presenters Offset = scrollable.Offset; } } + + private (IControl, Rect) CalculateCurrentAnchor() + { + if (_anchorCandidates == null) + { + return default; + } + + var bestCandidate = default(IControl); + var bestCandidateDistance = double.MaxValue; + + // Find the anchor candidate that is scrolled closest to the top-left of this + // ScrollContentPresenter. + foreach (var element in _anchorCandidates) + { + if (element.IsVisible && GetViewportBounds(element, out var bounds)) + { + var distance = (Vector)bounds.Position; + var candidateDistance = Math.Abs(distance.Length); + + if (candidateDistance < bestCandidateDistance) + { + bestCandidate = element; + bestCandidateDistance = candidateDistance; + } + } + } + + if (bestCandidate != null) + { + // We have a candidate, calculate its bounds relative to Child. Because these + // bounds aren't relative to the ScrollContentPresenter itself, if they change + // then we know it wasn't just due to scrolling. + var unscrolledBounds = TranslateBounds(bestCandidate, Child); + return (bestCandidate, unscrolledBounds); + } + + return default; + } + + private bool GetViewportBounds(IControl element, out Rect bounds) + { + if (TranslateBounds(element, Child, out var childBounds)) + { + // We want the bounds relative to the new Offset, regardless of whether the child + // control has actually been arranged to this offset yet, so translate first to the + // child control and then apply Offset rather than translating directly to this + // control. + var thisBounds = new Rect(Bounds.Size); + bounds = new Rect(childBounds.Position - Offset, childBounds.Size); + return bounds.Intersects(thisBounds); + } + + bounds = default; + return false; + } + + private Rect TranslateBounds(IControl control, IControl to) + { + if (TranslateBounds(control, to, out var bounds)) + { + return bounds; + } + + throw new InvalidOperationException("The control's bounds could not be translated to the requested control."); + } + + private bool TranslateBounds(IControl control, IControl to, out Rect bounds) + { + if (!control.IsVisible) + { + bounds = default; + return false; + } + + var p = control.TranslatePoint(default, to); + bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; + return p.HasValue; + } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 7a43d38d8a..e55bdbb0eb 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,8 +1,9 @@ -using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls.Primitives { /// @@ -13,36 +14,36 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty HeaderProperty = - AvaloniaProperty.Register(nameof(Header)); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); /// /// Defines the property. /// - public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); /// /// Initializes static members of the class. /// static HeaderedContentControl() { - ContentProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); + HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e)); } /// /// Gets or sets the header content. /// - public object Header + public object? Header { - get { return GetValue(HeaderProperty); } - set { SetValue(HeaderProperty, value); } + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } /// /// Gets the header presenter from the control's template. /// - public IContentPresenter HeaderPresenter + public IContentPresenter? HeaderPresenter { get; private set; @@ -51,10 +52,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the data template used to display the header content of the control. /// - public IDataTemplate HeaderTemplate + public IDataTemplate? HeaderTemplate { - get { return GetValue(HeaderTemplateProperty); } - set { SetValue(HeaderTemplateProperty, value); } + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); } /// 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/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index f6186a29a9..fc82fcc7a7 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -3,6 +3,7 @@ using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls.Primitives { @@ -40,10 +41,26 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsExpandedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsExpanded), + o => o.IsExpanded); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowAutoHideProperty = + AvaloniaProperty.Register(nameof(AllowAutoHide), true); + private Button _lineUpButton; private Button _lineDownButton; private Button _pageUpButton; private Button _pageDownButton; + private DispatcherTimer _timer; + private bool _isExpanded; /// /// Initializes static members of the class. @@ -90,6 +107,24 @@ namespace Avalonia.Controls.Primitives set { SetValue(OrientationProperty, value); } } + /// + /// Gets a value that indicates whether the scrollbar is expanded. + /// + public bool IsExpanded + { + get => _isExpanded; + private set => SetAndRaise(IsExpandedProperty, ref _isExpanded, value); + } + + /// + /// Gets a value that indicates whether the scrollbar can hide itself when user is not interacting with it. + /// + public bool AllowAutoHide + { + get => GetValue(AllowAutoHideProperty); + set => SetValue(AllowAutoHideProperty, value); + } + public event EventHandler Scroll; /// @@ -131,6 +166,10 @@ namespace Avalonia.Controls.Primitives { UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); } + else if (change.Property == AllowAutoHideProperty) + { + UpdateIsExpandedState(); + } else { if (change.Property == MinimumProperty || @@ -143,6 +182,26 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnPointerEnter(PointerEventArgs e) + { + base.OnPointerEnter(e); + + if (AllowAutoHide) + { + ExpandAfterDelay(); + } + } + + protected override void OnPointerLeave(PointerEventArgs e) + { + base.OnPointerLeave(e); + + if (AllowAutoHide) + { + CollapseAfterDelay(); + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_lineUpButton != null) @@ -170,8 +229,6 @@ namespace Avalonia.Controls.Primitives _pageUpButton = e.NameScope.Find public class ProgressBar : RangeBase { + public class ProgressBarTemplateProperties : AvaloniaObject + { + private double _container2Width; + private double _containerWidth; + private double _containerAnimationStartPosition; + private double _containerAnimationEndPosition; + private double _container2AnimationStartPosition; + private double _container2AnimationEndPosition; + + public static readonly DirectProperty ContainerAnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerAnimationStartPosition), + p => p.ContainerAnimationStartPosition, + (p, o) => p.ContainerAnimationStartPosition = o, 0d); + + public static readonly DirectProperty ContainerAnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerAnimationEndPosition), + p => p.ContainerAnimationEndPosition, + (p, o) => p.ContainerAnimationEndPosition = o, 0d); + + public static readonly DirectProperty Container2AnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2AnimationStartPosition), + p => p.Container2AnimationStartPosition, + (p, o) => p.Container2AnimationStartPosition = o, 0d); + + public static readonly DirectProperty Container2AnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2AnimationEndPosition), + p => p.Container2AnimationEndPosition, + (p, o) => p.Container2AnimationEndPosition = o); + + public static readonly DirectProperty Container2WidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2Width), + p => p.Container2Width, + (p, o) => p.Container2Width = o); + + public static readonly DirectProperty ContainerWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerWidth), + p => p.ContainerWidth, + (p, o) => p.ContainerWidth = o); + + public double ContainerAnimationStartPosition + { + get => _containerAnimationStartPosition; + set => SetAndRaise(ContainerAnimationStartPositionProperty, ref _containerAnimationStartPosition, value); + } + + public double ContainerAnimationEndPosition + { + get => _containerAnimationEndPosition; + set => SetAndRaise(ContainerAnimationEndPositionProperty, ref _containerAnimationEndPosition, value); + } + + public double Container2AnimationStartPosition + { + get => _container2AnimationStartPosition; + set => SetAndRaise(Container2AnimationStartPositionProperty, ref _container2AnimationStartPosition, value); + } + + public double Container2Width + { + get => _container2Width; + set => SetAndRaise(Container2WidthProperty, ref _container2Width, value); + } + + public double ContainerWidth + { + get => _containerWidth; + set => SetAndRaise(ContainerWidthProperty, ref _containerWidth, value); + } + + public double Container2AnimationEndPosition + { + get => _container2AnimationEndPosition; + set => SetAndRaise(Container2AnimationEndPositionProperty, ref _container2AnimationEndPosition, value); + } + } + + private double _indeterminateStartingOffset; + private double _indeterminateEndingOffset; + private Border _indicator; + public static readonly StyledProperty IsIndeterminateProperty = AvaloniaProperty.Register(nameof(IsIndeterminate)); @@ -20,19 +105,33 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); - private static readonly DirectProperty IndeterminateStartingOffsetProperty = + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateStartingOffset), p => p.IndeterminateStartingOffset, (p, o) => p.IndeterminateStartingOffset = o); - private static readonly DirectProperty IndeterminateEndingOffsetProperty = + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public static readonly DirectProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateEndingOffset), p => p.IndeterminateEndingOffset, (p, o) => p.IndeterminateEndingOffset = o); - private Border _indicator; + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public double IndeterminateStartingOffset + { + get => _indeterminateStartingOffset; + set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + } + + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public double IndeterminateEndingOffset + { + get => _indeterminateEndingOffset; + set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + } static ProgressBar() { @@ -45,6 +144,8 @@ namespace Avalonia.Controls UpdatePseudoClasses(IsIndeterminate, Orientation); } + public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties(); + public bool IsIndeterminate { get => GetValue(IsIndeterminateProperty); @@ -62,19 +163,6 @@ namespace Avalonia.Controls get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } - private double _indeterminateStartingOffset; - private double IndeterminateStartingOffset - { - get => _indeterminateStartingOffset; - set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); - } - - private double _indeterminateEndingOffset; - private double IndeterminateEndingOffset - { - get => _indeterminateEndingOffset; - set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); - } /// protected override Size ArrangeOverride(Size finalSize) @@ -111,21 +199,33 @@ namespace Avalonia.Controls { if (IsIndeterminate) { - if (Orientation == Orientation.Horizontal) - { - var width = bounds.Width / 5.0; - IndeterminateStartingOffset = -width; - _indicator.Width = width; - IndeterminateEndingOffset = bounds.Width; + // Pulled from ModernWPF. - } - else - { - var height = bounds.Height / 5.0; - IndeterminateStartingOffset = -bounds.Height; - _indicator.Height = height; - IndeterminateEndingOffset = height; - } + var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height; + var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar + var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar + + TemplateProperties.ContainerWidth = barIndicatorWidth; + TemplateProperties.Container2Width = barIndicatorWidth2; + + TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180% + TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300% + + TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150% + TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% + + // Remove these properties when we switch to fluent as default and removed the old one. + IndeterminateStartingOffset = -(dim / 5d); + IndeterminateEndingOffset = dim; + + var padding = Padding; + var rectangle = new RectangleGeometry( + new Rect( + padding.Left, + padding.Top, + bounds.Width - (padding.Right + padding.Left), + bounds.Height - (padding.Bottom + padding.Top) + )); } else { 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/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 2c870b4efd..87f4760156 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -80,6 +81,7 @@ namespace Avalonia.Controls static ItemsRepeater() { ClipToBoundsProperty.OverrideDefaultValue(true); + RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } /// @@ -305,6 +307,7 @@ namespace Avalonia.Controls virtInfo.AutoRecycleCandidate && !virtInfo.KeepAlive) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "AutoClear - {Index}", virtInfo.Index); ClearElementImpl(element); } } @@ -743,6 +746,11 @@ namespace Avalonia.Controls } } + private void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) + { + _viewportManager.OnBringIntoViewRequested(e); + } + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs index 977d9d794c..fd33eeaf39 100644 --- a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Layout; +using Avalonia.Logging; namespace Avalonia.Controls { @@ -58,7 +59,11 @@ namespace Avalonia.Controls protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); - protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); + protected override void RecycleElementCore(ILayoutable element) + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "RepeaterLayout - RecycleElement: {Index}", _owner.GetElementIndex((IControl)element)); + _owner.ClearElementImpl((IControl)element); + } protected override Rect RealizationRectCore() => _owner.RealizationWindow; } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 4ed9cb3333..eff51804b9 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -60,11 +61,13 @@ namespace Avalonia.Controls if (suppressAutoRecycle) { virtInfo.AutoRecycleCandidate = false; + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index); } else { virtInfo.AutoRecycleCandidate = true; virtInfo.KeepAlive = true; + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index); } return element; @@ -107,10 +110,28 @@ namespace Avalonia.Controls } } + // We need to clear the datacontext to prevent crashes from happening, + // however we only do that if we were the ones setting it. + // That is when one of the following is the case (numbering taken from line ~642): + // 1.2 No ItemTemplate, data is not a UIElement + // 2.1 ItemTemplate, data is not FrameworkElement + // 2.2.2 Itemtemplate, data is FrameworkElement, ElementFactory returned Element different to data + // + // In all of those three cases, we the ItemTemplateShim is NOT null. + // Luckily when we create the items, we store whether we were the once setting the DataContext. public void ClearElementToElementFactory(IControl element) { _owner.OnElementClearing(element); + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + virtInfo.MoveOwnershipToElementFactory(); + + // During creation of this object, we were the one setting the DataContext, so clear it now. + if (virtInfo.MustClearDataContext) + { + element.DataContext = null; + } + if (_owner.ItemTemplateShim != null) { _owner.ItemTemplateShim.RecycleElement(_owner, element); @@ -124,9 +145,6 @@ namespace Avalonia.Controls } } - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - virtInfo.MoveOwnershipToElementFactory(); - if (_lastFocusedElement == element) { // Focused element is going away. Remove the tracked last focused element @@ -594,11 +612,14 @@ namespace Avalonia.Controls { virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } + // Clear flag + virtInfo.MustClearDataContext = false; if (data != element) { // Prepare the element element.DataContext = data; + virtInfo.MustClearDataContext = true; } virtInfo.MoveOwnershipToLayoutFromElementFactory( diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 10c11889d0..0d22187b34 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -5,9 +5,15 @@ using System; using System.Collections.Generic; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Controls.Presenters; +using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.VisualTree; @@ -35,16 +41,16 @@ namespace Avalonia.Controls // actually happened. This can happen in cases where no scrollviewer // in the parent chain can scroll in the shift direction. private Point _unshiftableShift; - private double _maximumHorizontalCacheLength = 0.0; - private double _maximumVerticalCacheLength = 0.0; + private double _maximumHorizontalCacheLength = 2.0; + private double _maximumVerticalCacheLength = 2.0; private double _horizontalCacheBufferPerSide; private double _verticalCacheBufferPerSide; private bool _isBringIntoViewInProgress; // 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) @@ -184,6 +190,9 @@ namespace Avalonia.Controls // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})", + _owner.Layout.LayoutId, _expectedViewportShift); + // There are cases where we might be expecting a shift but not get it. We will // be waiting for the effective viewport event but if the scroll viewer is not able // to perform the shift (perhaps because it cannot scroll in negative offset), @@ -219,24 +228,26 @@ 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; } } public void OnElementPrepared(IControl element) { - // If we have an anchor element, we do not want the - // scroll anchor provider to start anchoring some other element. - ////element.CanBeScrollAnchor(true); + _scroller?.RegisterAnchorCandidate(element); } - public void OnElementCleared(ILayoutable element) + public void OnElementCleared(IControl element) { - ////element.CanBeScrollAnchor(false); + _scroller?.UnregisterAnchorCandidate(element); } public void OnOwnerMeasuring() @@ -282,6 +293,7 @@ namespace Avalonia.Controls private void OnLayoutUpdated(object sender, EventArgs args) { _owner.LayoutUpdated -= OnLayoutUpdated; + _layoutUpdatedSubscribed = false; if (_managingViewportDisabled) { return; @@ -293,6 +305,10 @@ namespace Avalonia.Controls // that can scroll in the direction where the shift is expected. if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure", + _owner.Layout.LayoutId, + _pendingViewportShift); + // Assume this is never going to come. _unshiftableShift = new Point( _unshiftableShift.X + _pendingViewportShift.X, @@ -306,8 +322,11 @@ namespace Avalonia.Controls public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) { - _makeAnchorElement = anchor; - _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + if (_makeAnchorElement != anchor) + { + _makeAnchorElement = anchor; + _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + } } public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) @@ -325,29 +344,36 @@ 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) { - ////if (child.CanBeScrollAnchor && child != targetChild) - ////{ - //// child.CanBeScrollAnchor = false; - ////} + if (child != targetChild) + { + _scroller.UnregisterAnchorCandidate(child); + } } - // Register to rendering event to go back to how things were before where any child can be the anchor. - _isBringIntoViewInProgress = true; - ////if (!m_renderingToken) - ////{ - //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; - //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); - ////} + // Register action to go back to how things were before where any child can be the anchor. Here, + // WinUI uses CompositionTarget.Rendering but we don't currently have that, so post an action to + // run *after* rendering has completed (priority needs to be lower than Render as Transformed + // bounds must have been set in order for OnEffectiveViewportChanged to trigger). + if (!_isBringIntoViewInProgress) + { + _isBringIntoViewInProgress = true; + Dispatcher.UIThread.Post(OnCompositionTargetRendering, DispatcherPriority.Loaded); + } } } private IControl GetImmediateChildOfRepeater(IControl descendant) { var targetChild = descendant; - var parent = descendant.Parent; + var parent = (IControl)descendant.VisualParent; while (parent != null && parent != _owner) { targetChild = parent; @@ -356,33 +382,57 @@ 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; } - public void ResetScrollers() + private void OnCompositionTargetRendering() { - _scroller = null; - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = null; - _ensuredScroller = false; + _isBringIntoViewInProgress = false; + _makeAnchorElement = null; + + if (_scroller is object) + { + foreach (var child in _owner.Children) + { + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (info.IsRealized && info.IsHeldByLayout) + { + _scroller.RegisterAnchorCandidate(child); + } + } + } + + // HACK: Invalidate measure now that the anchor has been removed so that a layout can be + // done with a proper realization rect. This is a hack not present upstream to try to fix + // https://github.com/microsoft/microsoft-ui-xaml/issues/1422 + TryInvalidateMeasure(); } - private void OnEffectiveViewportChanged(TransformedBounds? bounds) + public void ResetScrollers() { - if (!bounds.HasValue) + if (_scroller is object) { - return; + foreach (var child in _owner.Children) + { + _scroller.UnregisterAnchorCandidate(child); + } + + _scroller = null; } - var globalClip = bounds.Value.Clip; - var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; - var clip = globalClip.TransformToAABB(transform); - var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = false; + _ensuredScroller = false; + } - UpdateViewport(effectiveViewport); + private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e) + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); + UpdateViewport(e.EffectiveViewport); _pendingViewportShift = default; _unshiftableShift = default; @@ -397,6 +447,7 @@ namespace Avalonia.Controls if (_layoutUpdatedSubscribed) { _owner.LayoutUpdated -= OnLayoutUpdated; + _layoutUpdatedSubscribed = false; } } @@ -426,8 +477,8 @@ namespace Avalonia.Controls } else if (!_managingViewportDisabled) { - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + _owner.EffectiveViewportChanged += OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = true; } _ensuredScroller = true; @@ -437,10 +488,17 @@ namespace Avalonia.Controls private void UpdateViewport(Rect viewport) { var currentVisibleWindow = viewport; + var previousVisibleWindow = _visibleWindow; + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Effective Viewport: ({Before})->({After})", + _owner.Layout.LayoutId, + previousVisibleWindow, + viewport); if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout.LayoutId); // We got cleared. _visibleWindow = default; } @@ -449,7 +507,14 @@ namespace Avalonia.Controls _visibleWindow = currentVisibleWindow; } - TryInvalidateMeasure(); + if (_visibleWindow != previousVisibleWindow) + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Used Viewport: ({Before})->({After})", + _owner.Layout.LayoutId, + previousVisibleWindow, + currentVisibleWindow); + TryInvalidateMeasure(); + } } private static void ValidateCacheLength(double cacheLength) @@ -468,26 +533,11 @@ namespace Avalonia.Controls // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to // avoid layout cycles. + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout.LayoutId); _owner.InvalidateMeasure(); } } - 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, resulting in the UI being updated too late when scrolling quickly. We can - // partially remedey this by triggering also on Bounds changes, but this won't work so - // well for nested ItemsRepeaters. - // - // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially - // for this case): we need to implement that in Avalonia. - return control.GetObservable(Visual.TransformedBoundsProperty) - .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) - .Skip(1) - .Subscribe(OnEffectiveViewportChanged); - } - private class ScrollerInfo { public ScrollerInfo(ScrollViewer scroller) diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index eb30c1b7cf..7a639419c1 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -36,6 +36,7 @@ namespace Avalonia.Controls public bool IsHeldByLayout => Owner == ElementOwner.Layout; public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; + public bool MustClearDataContext { get; set; } public bool KeepAlive { get; set; } public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public string UniqueId { get; private set; } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index a5f55eaa02..4600301410 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -166,6 +167,18 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsExpandedProperty = + ScrollBar.IsExpandedProperty.AddOwner(o => o.IsExpanded); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowAutoHideProperty = + ScrollBar.AllowAutoHideProperty.AddOwner(); + /// /// Defines the event. /// @@ -186,6 +199,8 @@ namespace Avalonia.Controls private Size _oldViewport; private Size _largeChange; private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange); + private bool _isExpanded; + private IDisposable _scrollBarExpandSubscription; /// /// Initializes static members of the class. @@ -244,9 +259,7 @@ namespace Avalonia.Controls set { - value = ValidateOffset(this, value); - - if (SetAndRaise(OffsetProperty, ref _offset, value)) + if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value))) { CalculatedPropertiesChanged(); } @@ -316,6 +329,9 @@ namespace Avalonia.Controls get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; } } + /// + public IControl CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor; + /// /// Gets the maximum horizontal scrollbar value. /// @@ -382,8 +398,23 @@ namespace Avalonia.Controls get { return _viewport.Height; } } - /// - IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// + /// Gets a value that indicates whether any scrollbar is expanded. + /// + public bool IsExpanded + { + get => _isExpanded; + private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value); + } + + /// + /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it. + /// + public bool AllowAutoHide + { + get => GetValue(AllowAutoHideProperty); + set => SetValue(AllowAutoHideProperty, value); + } /// /// Scrolls the content up one line. @@ -473,14 +504,16 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } - void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + /// + public void RegisterAnchorCandidate(IControl element) { - // TODO: Implement + (Presenter as IScrollAnchorProvider)?.RegisterAnchorCandidate(element); } - void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + /// + public void UnregisterAnchorCandidate(IControl element) { - // TODO: Implement + (Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element); } protected override bool RegisterContentPresenter(IContentPresenter presenter) @@ -517,22 +550,6 @@ namespace Avalonia.Controls return double.IsNaN(result) ? 0 : result; } - private static Vector ValidateOffset(AvaloniaObject o, Vector value) - { - ScrollViewer scrollViewer = o as ScrollViewer; - - if (scrollViewer != null) - { - var extent = scrollViewer.Extent; - var viewport = scrollViewer.Viewport; - return CoerceOffset(extent, viewport, value); - } - else - { - return value; - } - } - private void ChildChanged(IControl child) { if (_logicalScrollable is object) @@ -630,6 +647,54 @@ namespace Avalonia.Controls RaiseEvent(e); } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _scrollBarExpandSubscription?.Dispose(); + + _scrollBarExpandSubscription = SubscribeToScrollBars(e); + } + + private IDisposable SubscribeToScrollBars(TemplateAppliedEventArgs e) + { + static IObservable GetExpandedObservable(ScrollBar scrollBar) + { + return scrollBar?.GetObservable(ScrollBar.IsExpandedProperty); + } + + var horizontalScrollBar = e.NameScope.Find("PART_HorizontalScrollBar"); + var verticalScrollBar = e.NameScope.Find("PART_VerticalScrollBar"); + + var horizontalExpanded = GetExpandedObservable(horizontalScrollBar); + var verticalExpanded = GetExpandedObservable(verticalScrollBar); + + IObservable actualExpanded = null; + + if (horizontalExpanded != null && verticalExpanded != null) + { + actualExpanded = horizontalExpanded.CombineLatest(verticalExpanded, (h, v) => h || v); + } + else + { + if (horizontalExpanded != null) + { + actualExpanded = horizontalExpanded; + } + else if (verticalExpanded != null) + { + actualExpanded = verticalExpanded; + } + } + + return actualExpanded?.Subscribe(OnScrollBarExpandedChanged); + } + + private void OnScrollBarExpandedChanged(bool isExpanded) + { + IsExpanded = isExpanded; + } + private void OnLayoutUpdated(object sender, EventArgs e) => RaiseScrollChanged(); private void RaiseScrollChanged() diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index ff1c0260bb..aa6552579f 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -189,8 +189,6 @@ namespace Avalonia.Controls } set { - var isSelected = IsSelectedWithPartialAt(value); - if (!IsSelectedAt(value) || SelectedItems.Count > 1) { using var operation = new Operation(this); diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs new file mode 100644 index 0000000000..fc3ff51f24 --- /dev/null +++ b/src/Avalonia.Controls/SplitView.cs @@ -0,0 +1,487 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.VisualTree; +using System; +using System.Reactive.Disposables; + +namespace Avalonia.Controls +{ + /// + /// Defines constants for how the SplitView Pane should display + /// + public enum SplitViewDisplayMode + { + /// + /// Pane is displayed next to content, and does not auto collapse + /// when tapped outside + /// + Inline, + /// + /// Pane is displayed next to content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane does not auto collapse + /// when tapped outside + /// + CompactInline, + /// + /// Pane is displayed above content. Pane collapses when tapped outside + /// + Overlay, + /// + /// Pane is displayed above content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane collapses when tapped outside + /// + CompactOverlay + } + + /// + /// Defines constants for where the Pane should appear + /// + public enum SplitViewPanePlacement + { + Left, + Right + } + + public class SplitViewTemplateSettings : AvaloniaObject + { + internal SplitViewTemplateSettings() { } + + public static readonly StyledProperty ClosedPaneWidthProperty = + AvaloniaProperty.Register(nameof(ClosedPaneWidth), 0d); + + public static readonly StyledProperty PaneColumnGridLengthProperty = + AvaloniaProperty.Register(nameof(PaneColumnGridLength)); + + public double ClosedPaneWidth + { + get => GetValue(ClosedPaneWidthProperty); + internal set => SetValue(ClosedPaneWidthProperty, value); + } + + public GridLength PaneColumnGridLength + { + get => GetValue(PaneColumnGridLengthProperty); + internal set => SetValue(PaneColumnGridLengthProperty, value); + } + } + + /// + /// A control with two views: A collapsible pane and an area for content + /// + public class SplitView : TemplatedControl + { + /* + Pseudo classes & combos + :open / :closed + :compactoverlay :compactinline :overlay :inline + :left :right + */ + + /// + /// Defines the property + /// + public static readonly StyledProperty ContentProperty = + AvaloniaProperty.Register(nameof(Content)); + + /// + /// Defines the property + /// + public static readonly StyledProperty CompactPaneLengthProperty = + AvaloniaProperty.Register(nameof(CompactPaneLength), defaultValue: 48); + + /// + /// Defines the property + /// + public static readonly StyledProperty DisplayModeProperty = + AvaloniaProperty.Register(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay); + + /// + /// Defines the property + /// + public static readonly DirectProperty IsPaneOpenProperty = + AvaloniaProperty.RegisterDirect(nameof(IsPaneOpen), + x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v); + + /// + /// Defines the property + /// + public static readonly StyledProperty OpenPaneLengthProperty = + AvaloniaProperty.Register(nameof(OpenPaneLength), defaultValue: 320); + + /// + /// Defines the property + /// + public static readonly StyledProperty PaneBackgroundProperty = + AvaloniaProperty.Register(nameof(PaneBackground)); + + /// + /// Defines the property + /// + public static readonly StyledProperty PanePlacementProperty = + AvaloniaProperty.Register(nameof(PanePlacement)); + + /// + /// Defines the property + /// + public static readonly StyledProperty PaneProperty = + AvaloniaProperty.Register(nameof(Pane)); + + /// + /// Defines the property + /// + public static readonly StyledProperty UseLightDismissOverlayModeProperty = + AvaloniaProperty.Register(nameof(UseLightDismissOverlayMode)); + + /// + /// Defines the property + /// + public static readonly StyledProperty TemplateSettingsProperty = + AvaloniaProperty.Register(nameof(TemplateSettings)); + + private bool _isPaneOpen; + private Panel _pane; + private CompositeDisposable _pointerDisposables; + + public SplitView() + { + PseudoClasses.Add(":overlay"); + PseudoClasses.Add(":left"); + + TemplateSettings = new SplitViewTemplateSettings(); + } + + static SplitView() + { + UseLightDismissOverlayModeProperty.Changed.AddClassHandler((x, v) => x.OnUseLightDismissChanged(v)); + CompactPaneLengthProperty.Changed.AddClassHandler((x, v) => x.OnCompactPaneLengthChanged(v)); + PanePlacementProperty.Changed.AddClassHandler((x, v) => x.OnPanePlacementChanged(v)); + DisplayModeProperty.Changed.AddClassHandler((x, v) => x.OnDisplayModeChanged(v)); + } + + /// + /// Gets or sets the content of the SplitView + /// + [Content] + public IControl Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// Gets or sets the length of the pane when in + /// or mode + /// + public double CompactPaneLength + { + get => GetValue(CompactPaneLengthProperty); + set => SetValue(CompactPaneLengthProperty, value); + } + + /// + /// Gets or sets the for the SplitView + /// + public SplitViewDisplayMode DisplayMode + { + get => GetValue(DisplayModeProperty); + set => SetValue(DisplayModeProperty, value); + } + + /// + /// Gets or sets whether the pane is open or closed + /// + public bool IsPaneOpen + { + get => _isPaneOpen; + set + { + if (value == _isPaneOpen) + { + return; + } + + if (value) + { + OnPaneOpening(this, null); + SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); + + PseudoClasses.Add(":open"); + PseudoClasses.Remove(":closed"); + OnPaneOpened(this, null); + } + else + { + SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false); + OnPaneClosing(this, args); + if (!args.Cancel) + { + SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); + + PseudoClasses.Add(":closed"); + PseudoClasses.Remove(":open"); + OnPaneClosed(this, null); + } + } + } + } + + /// + /// Gets or sets the length of the pane when open + /// + public double OpenPaneLength + { + get => GetValue(OpenPaneLengthProperty); + set => SetValue(OpenPaneLengthProperty, value); + } + + /// + /// Gets or sets the background of the pane + /// + public IBrush PaneBackground + { + get => GetValue(PaneBackgroundProperty); + set => SetValue(PaneBackgroundProperty, value); + } + + /// + /// Gets or sets the for the SplitView + /// + public SplitViewPanePlacement PanePlacement + { + get => GetValue(PanePlacementProperty); + set => SetValue(PanePlacementProperty, value); + } + + /// + /// Gets or sets the Pane for the SplitView + /// + public IControl Pane + { + get => GetValue(PaneProperty); + set => SetValue(PaneProperty, value); + } + + /// + /// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled + /// When enabled, and the pane is open in Overlay or CompactOverlay mode, + /// the contents of the splitview are darkened to visually separate the open pane + /// and the rest of the SplitView + /// + public bool UseLightDismissOverlayMode + { + get => GetValue(UseLightDismissOverlayModeProperty); + set => SetValue(UseLightDismissOverlayModeProperty, value); + } + + /// + /// Gets or sets the TemplateSettings for the SplitView + /// + public SplitViewTemplateSettings TemplateSettings + { + get => GetValue(TemplateSettingsProperty); + set => SetValue(TemplateSettingsProperty, value); + } + + /// + /// Fired when the pane is closed + /// + public event EventHandler PaneClosed; + + /// + /// Fired when the pane is closing + /// + public event EventHandler PaneClosing; + + /// + /// Fired when the pane is opened + /// + public event EventHandler PaneOpened; + + /// + /// Fired when the pane is opening + /// + public event EventHandler PaneOpening; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _pane = e.NameScope.Find("PART_PaneRoot"); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var topLevel = this.VisualRoot; + if (topLevel is Window window) + { + //Logic adapted from Popup + //Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane + IDisposable subscribeToEventHandler(T target, TEventHandler handler, + Action subscribe, Action unsubscribe) + { + subscribe(target, handler); + return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); + } + + _pointerDisposables = new CompositeDisposable( + window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel), + InputManager.Instance?.Process.Subscribe(OnNonClientClick), + subscribeToEventHandler(window, Window_Deactivated, + (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler), + subscribeToEventHandler(window.PlatformImpl, OnWindowLostFocus, + (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler)); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _pointerDisposables?.Dispose(); + } + + private void OnWindowLostFocus() + { + if (IsPaneOpen && ShouldClosePane()) + { + IsPaneOpen = false; + } + } + + private void PointerPressedOutside(object sender, PointerPressedEventArgs e) + { + if (!IsPaneOpen) + { + return; + } + + //If we click within the Pane, don't do anything + //Otherwise, ClosePane if open & using an overlay display mode + bool closePane = ShouldClosePane(); + if (!closePane) + { + return; + } + + var src = e.Source as IVisual; + while (src != null) + { + if (src == _pane) + { + closePane = false; + break; + } + + src = src.VisualParent; + } + if (closePane) + { + IsPaneOpen = false; + e.Handled = true; + } + } + + private void OnNonClientClick(RawInputEventArgs obj) + { + if (!IsPaneOpen) + { + return; + } + + var mouse = obj as RawPointerEventArgs; + if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) + + { + if (ShouldClosePane()) + IsPaneOpen = false; + } + } + + private void Window_Deactivated(object sender, EventArgs e) + { + if (IsPaneOpen && ShouldClosePane()) + { + IsPaneOpen = false; + } + } + + private bool ShouldClosePane() + { + return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); + } + + protected virtual void OnPaneOpening(SplitView sender, EventArgs args) + { + PaneOpening?.Invoke(sender, args); + } + + protected virtual void OnPaneOpened(SplitView sender, EventArgs args) + { + PaneOpened?.Invoke(sender, args); + } + + protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args) + { + PaneClosing?.Invoke(sender, args); + } + + protected virtual void OnPaneClosed(SplitView sender, EventArgs args) + { + PaneClosed?.Invoke(sender, args); + } + + private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e) + { + var newLen = (double)e.NewValue; + var displayMode = DisplayMode; + if (displayMode == SplitViewDisplayMode.CompactInline) + { + TemplateSettings.ClosedPaneWidth = newLen; + } + else if (displayMode == SplitViewDisplayMode.CompactOverlay) + { + TemplateSettings.ClosedPaneWidth = newLen; + TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel); + } + } + + private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) + { + var oldState = e.OldValue.ToString().ToLower(); + var newState = e.NewValue.ToString().ToLower(); + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + } + + private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) + { + var oldState = e.OldValue.ToString().ToLower(); + var newState = e.NewValue.ToString().ToLower(); + + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + + var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch + { + SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), + SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), + SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)), + SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)), + _ => throw new NotImplementedException(), + }; + TemplateSettings.ClosedPaneWidth = closedPaneWidth; + TemplateSettings.PaneColumnGridLength = paneColumnGridLength; + } + + private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e) + { + var mode = (bool)e.NewValue; + PseudoClasses.Set(":lightdismiss", mode); + } + } +} diff --git a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs new file mode 100644 index 0000000000..46fb2d161b --- /dev/null +++ b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Controls +{ + public class SplitViewPaneClosingEventArgs : EventArgs + { + public bool Cancel { get; set; } + + public SplitViewPaneClosingEventArgs(bool cancel) + { + Cancel = cancel; + } + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d34444eb8..f058942116 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 . @@ -337,6 +340,9 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + Renderer?.Dispose(); + Renderer = null; + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -346,8 +352,8 @@ namespace Avalonia.Controls (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); - Renderer?.Dispose(); - Renderer = null; + + LayoutManager?.Dispose(); } /// @@ -471,5 +477,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/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index cd586f7701..4942d4d313 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -49,6 +49,7 @@ namespace Avalonia.Controls static TreeViewItem() { SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); ParentProperty.Changed.AddClassHandler((o, e) => o.OnParentChanged(e)); diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9b6444fc66..9614d079d9 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; namespace Avalonia.Controls.Utils @@ -15,12 +16,14 @@ namespace Avalonia.Controls.Utils { if (items != null) { - var collection = items as ICollection; - - if (collection != null) + if (items is ICollection collection) { return collection.Count; } + else if (items is IReadOnlyCollection readOnly) + { + return readOnly.Count; + } else { return Enumerable.Count(items.Cast()); 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.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index b3a8f4745e..4899be2955 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -45,7 +45,15 @@ namespace Avalonia.Diagnostics window.Closed += DevToolsClosed; s_open.Add(root, window); - window.Show(); + + if (root is Window inspectedWindow) + { + window.Show(inspectedWindow); + } + else + { + window.Show(); + } } return Disposable.Create(() => window?.Close()); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index a7c2997346..38788ef8ee 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; @@ -9,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public LogicalTreeNode(ILogical logical, TreeNode parent) : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); + Children = new LogicalTreeNodeCollection(this, logical); } public static LogicalTreeNode[] Create(object control) @@ -17,5 +18,31 @@ namespace Avalonia.Diagnostics.ViewModels var logical = control as ILogical; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; } + + internal class LogicalTreeNodeCollection : TreeNodeCollection + { + private readonly ILogical _control; + private IDisposable _subscription; + + public LogicalTreeNodeCollection(TreeNode owner, ILogical control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + base.Dispose(); + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.LogicalChildren.ForEachItem( + (i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1d19e1a346..acc3ef16c2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; + private readonly IDisposable _pointerOverSubscription; private ViewModelBase _content; private int _selectedTab; private string _focusedControl; @@ -25,16 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels _events = new EventsPageViewModel(root); UpdateFocusedControl(); - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - + KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; SelectedTab = 0; - root.GetObservable(TopLevel.PointerOverElementProperty) + _pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); Console = new ConsoleViewModel(UpdateConsoleContext); } @@ -129,6 +124,8 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { + KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; + _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); } @@ -137,5 +134,13 @@ namespace Avalonia.Diagnostics.ViewModels { FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } + + private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + UpdateFocusedControl(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index aa27538abc..cb5f5b1fda 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -3,15 +3,15 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ViewModelBase + internal class TreeNode : ViewModelBase, IDisposable { + private IDisposable _classesSubscription; private string _classes; private bool _isExpanded; @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels x => control.Classes.CollectionChanged -= x) .TakeUntil(removed); - classesChanged.Select(_ => Unit.Default) + _classesSubscription = classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IAvaloniaReadOnlyList Children + public TreeNodeCollection Children { get; protected set; @@ -104,6 +104,12 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void Dispose() + { + _classesSubscription.Dispose(); + Children.Dispose(); + } + private static int IndexOf(IReadOnlyList collection, TreeNode item) { var count = collection.Count; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs new file mode 100644 index 0000000000..8b4f03bd23 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Collections; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable + { + private AvaloniaList _inner; + + public TreeNodeCollection(TreeNode owner) => Owner = owner; + + public TreeNode this[int index] + { + get + { + EnsureInitialized(); + return _inner[index]; + } + } + + public int Count + { + get + { + EnsureInitialized(); + return _inner.Count; + } + } + + protected TreeNode Owner { get; } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _inner.CollectionChanged += value; + remove => _inner.CollectionChanged -= value; + } + + public event PropertyChangedEventHandler PropertyChanged + { + add => _inner.PropertyChanged += value; + remove => _inner.PropertyChanged -= value; + } + + public virtual void Dispose() + { + if (_inner is object) + { + foreach (var node in _inner) + { + node.Dispose(); + } + } + } + + public IEnumerator GetEnumerator() + { + EnsureInitialized(); + return _inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected abstract void Initialize(AvaloniaList nodes); + + private void EnsureInitialized() + { + if (_inner is null) + { + _inner = new AvaloniaList(); + Initialize(_inner); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 38ac88a83c..ec48cff399 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -62,7 +62,15 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void Dispose() => _details?.Dispose(); + public void Dispose() + { + foreach (var node in Nodes) + { + node.Dispose(); + } + + _details?.Dispose(); + } public TreeNode FindNode(IControl control) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 5383cb2b68..bc40edf477 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Styling; using Avalonia.VisualTree; @@ -9,16 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public VisualTreeNode(IVisual visual, TreeNode parent) : base(visual, parent) { - var host = visual as IVisualTreeHost; - - if (host?.Root == null) - { - Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this)); - } - else - { - Children = new AvaloniaList(new[] { new VisualTreeNode(host.Root, this) }); - } + Children = new VisualTreeNodeCollection(this, visual); if ((Visual is IStyleable styleable)) { @@ -33,5 +25,30 @@ namespace Avalonia.Diagnostics.ViewModels var visual = control as IVisual; return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; } + + internal class VisualTreeNodeCollection : TreeNodeCollection + { + private readonly IVisual _control; + private IDisposable _subscription; + + public VisualTreeNodeCollection(TreeNode owner, IVisual control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.VisualChildren.ForEachItem( + (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 3abdb5034a..10861538ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -14,8 +14,8 @@ namespace Avalonia.Diagnostics.Views { internal class MainWindow : Window, IStyleHost { + private readonly IDisposable _keySubscription; private TopLevel _root; - private IDisposable _keySubscription; public MainWindow() { @@ -33,8 +33,22 @@ namespace Avalonia.Diagnostics.Views { if (_root != value) { + if (_root != null) + { + _root.Closed -= RootClosed; + } + _root = value; - DataContext = new MainViewModel(value); + + if (_root != null) + { + _root.Closed += RootClosed; + DataContext = new MainViewModel(value); + } + else + { + DataContext = null; + } } } } @@ -45,6 +59,8 @@ namespace Avalonia.Diagnostics.Views { base.OnClosed(e); _keySubscription.Dispose(); + _root.Closed -= RootClosed; + _root = null; ((MainViewModel)DataContext)?.Dispose(); } @@ -70,5 +86,7 @@ namespace Avalonia.Diagnostics.Views } } } + + private void RootClosed(object sender, EventArgs e) => Close(); } } 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/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index d22566442a..047c01343f 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -12,7 +12,7 @@ namespace Avalonia.Layout /// public abstract class AttachedLayout : AvaloniaObject { - internal string LayoutId { get; set; } + public string LayoutId { get; set; } /// /// Occurs when the measurement state (layout) has been invalidated. 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/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 1748a3be03..70805ba31c 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Layout.Utils; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -78,6 +79,7 @@ namespace Avalonia.Layout { // Sentinel. Create the element now since we need it. int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex); element = _context.GetOrCreateElementAt( dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -232,6 +234,8 @@ namespace Avalonia.Layout { Insert(0, dataIndex, element); } + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex); } } diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 0d64f8dfd5..cd7f725f18 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -82,6 +83,9 @@ namespace Avalonia.Layout // If minor size is infinity, there is only one line and no need to align that line. _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize)); var realizationRect = RealizationRect; + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: MeasureLayout Realization({Rect})", + layoutId, + realizationRect); var suggestedAnchorIndex = _context.RecommendedAnchorIndex; if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) @@ -100,6 +104,7 @@ namespace Avalonia.Layout Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); if (isWrapping && IsReflowRequired()) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Reflow Pass", layoutId); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); @@ -121,6 +126,7 @@ namespace Avalonia.Layout LineAlignment lineAlignment, string layoutId) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: ArrangeLayout", layoutId); ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( @@ -184,6 +190,7 @@ namespace Avalonia.Layout if (isAnchorSuggestionValid) { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex); anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( suggestedAnchorIndex, availableSize, @@ -223,6 +230,9 @@ namespace Avalonia.Layout } else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) { + if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); } + if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Disconnected Window", layoutId); } + // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window // but not the visible window. In that situation, we still need to produce a valid anchor. var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context); @@ -231,6 +241,7 @@ namespace Avalonia.Layout } else { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId); // No suggestion - just pick first in realized range anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); @@ -238,12 +249,14 @@ namespace Avalonia.Layout } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex); _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; if (_elementManager.IsIndexValidInData(anchorIndex)) { if (!_elementManager.IsDataIndexRealized(anchorIndex)) { // Disconnected, throw everything and create new anchor + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId); _elementManager.ClearRealizedRange(); var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); @@ -254,9 +267,17 @@ namespace Avalonia.Layout var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})", + layoutId, + anchorIndex, + layoutBounds); } else { + // Throw everything away + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Anchor index is not valid - throwing away all realized elements", + layoutId); _elementManager.ClearRealizedRange(); } @@ -280,6 +301,12 @@ namespace Avalonia.Layout if (anchorIndex != -1) { int step = (direction == GenerateDirection.Forward) ? 1 : -1; + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Generating {Direction} from anchor {Anchor}", + layoutId, + direction, + anchorIndex); + int previousIndex = anchorIndex; int currentIndex = anchorIndex + step; var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); @@ -365,6 +392,10 @@ namespace Avalonia.Layout _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); _orientation.SetMajorSize(ref bounds, lineMajorSize); _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})", + layoutId, + dataIndex, + bounds); } } } @@ -387,6 +418,11 @@ namespace Avalonia.Layout } _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).", + layoutId, + currentIndex, + currentBounds); previousIndex = currentIndex; currentIndex += step; } @@ -394,20 +430,17 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not // account for that element in the indicies inside the realization window. - if (count > 0) + if (direction == GenerateDirection.Forward) { - if (direction == GenerateDirection.Forward) - { - int dataCount = _context.ItemCount; - _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; - _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); - } - else - { - int dataCount = _context.ItemCount; - _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; - _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); - } + int dataCount = _context.ItemCount; + _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; + _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); + } + else + { + int dataCount = _context.ItemCount; + _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; + _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); } _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); @@ -508,6 +541,7 @@ namespace Avalonia.Layout lastDataIndex, lastBounds); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Extent: ({Bounds})", layoutId, extent); return extent; } @@ -676,6 +710,11 @@ namespace Avalonia.Layout } var element = _elementManager.GetAt(rangeIndex); + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Arranging element {Index} at ({Bounds})", + layoutId, + _elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex), + bounds); element.Arrange(bounds); } } 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/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index d8fa00deb7..3708748ad1 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -118,7 +118,7 @@ namespace Avalonia.Layout double newValue; // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) + if (!MathUtilities.IsOne(dpiScale)) { newValue = Math.Round(value * dpiScale) / dpiScale; diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 1c0c736b78..fc988a8d6c 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,27 +13,37 @@ 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; } - public event EventHandler? LayoutUpdated; + public virtual event EventHandler? LayoutUpdated; /// - public void InvalidateMeasure(ILayoutable control) + public virtual void InvalidateMeasure(ILayoutable control) { control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -41,17 +54,27 @@ 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(); } /// - public void InvalidateArrange(ILayoutable control) + public virtual void InvalidateArrange(ILayoutable control) { control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -62,21 +85,27 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); + } + _toArrange.Enqueue(control); QueueLayoutPass(); } /// - public void ExecuteLayoutPass() + public virtual void ExecuteLayoutPass() { - const int MaxPasses = 3; - Dispatcher.UIThread.VerifyAccess(); - if (!_running) + if (_disposed) { - _running = true; + return; + } + if (!_running) + { Stopwatch? stopwatch = null; const LogEventLevel timingLogLevel = LogEventLevel.Information; @@ -99,12 +128,13 @@ namespace Avalonia.Layout try { + _running = true; + for (var pass = 0; pass < MaxPasses; ++pass) { - ExecuteMeasurePass(); - ExecuteArrangePass(); + InnerLayoutPass(); - if (_toMeasure.Count == 0) + if (!RaiseEffectiveViewportChanged()) { break; } @@ -131,13 +161,18 @@ namespace Avalonia.Layout } /// - public 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 +186,60 @@ 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() + { + for (var pass = 0; pass < MaxPasses; ++pass) + { + ExecuteMeasurePass(); + ExecuteArrangePass(); + + if (_toMeasure.Count == 0) + { + break; + } + } + } + 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 + _toArrange.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 96f893e7b0..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 { @@ -77,12 +77,19 @@ namespace Avalonia.Layout { if (_shouldEnqueue(item.Key)) { - _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; + _loopQueueInfo[item.Key] = new Info() { Active = true, Count = 0 }; _inner.Enqueue(item.Key); } } _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.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs index 0b730315e1..6fa8601916 100644 --- a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs @@ -112,7 +112,9 @@ namespace Avalonia.Layout u = (isVertical ? bounds.Bottom : bounds.Right) + spacing; } - return new Size(bounds.Right, bounds.Bottom); + return new Size( + Math.Max(finalSize.Width, bounds.Width), + Math.Max(finalSize.Height, bounds.Height)); } private static Rect LayoutVertical(ILayoutable element, double y, Size constraint) diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 1a90d2a2e0..909c7bc7eb 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Specialized; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -107,8 +108,15 @@ namespace Avalonia.Layout _orientation.MajorStart(extent) + (remainingItems * averageElementSize)); } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", + LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", + LayoutId, extent.Size, averageElementSize); return extent; } diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 1698f61989..68f08d7cbb 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Specialized; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -379,8 +380,14 @@ namespace Avalonia.Layout ref extent, _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); + } } + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", + LayoutId, extent.Size, lineSize, itemsPerLine); return extent; } 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..a46528dc48 --- /dev/null +++ b/src/Avalonia.Native/NativeControlHostImpl.cs @@ -0,0 +1,135 @@ +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 HideWithSize(Size size) + { + _native.HideWithSize(Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height)); + } + + public void ShowInBounds(Rect bounds) + { + if (_attachedTo == null) + throw new InvalidOperationException("Native control isn't attached to a toplevel"); + bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), + Math.Max(1, bounds.Height)); + _native.ShowInBounds((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/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c41be1723b..b0da5fdc43 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -10,6 +10,7 @@ namespace Avalonia.Native private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly GlPlatformFeature _glFeature; + private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, @@ -19,6 +20,7 @@ namespace Avalonia.Native _factory = factory; _opts = opts; _glFeature = glFeature; + _parent = parent; using (var e = new PopupEvents(this)) { var context = _opts.UseGpu ? glFeature?.DeferredContext : null; @@ -58,6 +60,16 @@ namespace Avalonia.Native } } + public override void Show() + { + var parent = _parent; + while (parent is PopupImpl p) + parent = p._parent; + if (parent is WindowImpl w) + w.Native.TakeFocusFromChildren(); + base.Show(); + } + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); public void SetWindowManagerAddShadowHint(bool enabled) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 9783454e0e..9a90f65d1b 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(); } @@ -304,7 +319,7 @@ namespace Avalonia.Native } - public void Show() + public virtual void Show() { _native.Show(); } diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index d3c2f0c784..43a0752bc8 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,4 +1,12 @@ + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 9a1ced3db8..2b960259d0 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -2,36 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=netstandard"> - #FF0078D7 - #FFFFFFFF - #FFFFFFFF - #FFFFFFFF - #FFFFFFFF - #FFFFFFFF - #FF000000 - #FFCCCCCC - #FF898989 - #FF5D5D5D - #FF737373 - #FF5D5D5D - #FF000000 - #FFCCCCCC - #FF5D5D5D - #FF898989 - #FFCCCCCC - #FF898989 - #FF737373 - #FFCCCCCC - #FFECECEC - #FFE6E6E6 - #FFECECEC - #FFFFFFFF - #FFE6E6E6 - #FFCCCCCC - #CCFFFFFF - #FFCCCCCC - #FFE6E6E6 - #FFCCCCCC + #17000000 + #2E000000 @@ -68,13 +40,6 @@ 1,1,1,1 1 - - #FF005A9E - #FF004275 - #FF002642 - #FF429CE3 - #FF76B9ED - #FFA6D8FF #FFFFFFFF @@ -222,6 +187,12 @@ + + + + + + @@ -473,5 +444,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00.1 + 00:00:00.40 + 00:00:02.00 + 00:00:02 + 00:00:00.1 + 00:00:02.1 + 16 + 0.125 + -2 + scaleX(0.125) translateX(-2px) + scaleY(0.125) translateY(-2px) + 8 + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index b9502daaea..9f81db9af1 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -336,6 +336,15 @@ --> + + 0.6 + 4 + 0 + + + + + @@ -726,5 +735,100 @@ + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00.1 + 00:00:02 + 00:00:00.1 + 00:00:02.1 + 16 + 8 + 00:00:00.40 + 00:00:02.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index d16e6b6b55..94d1de7af6 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -248,6 +248,14 @@ + + 0.6 + 4 + 0 + + + + 1 @@ -724,5 +732,100 @@ + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00.1 + 00:00:02 + 00:00:00.1 + 00:00:02.1 + 16 + 8 + 00:00:00.40 + 00:00:02.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + 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.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 49b2d9561b..882eb0deae 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -53,4 +53,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml index 2d5e369573..f20dc2e650 100644 --- a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml @@ -1,10 +1,34 @@ - + + + 0 + 2 + 1 + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml index d3c2f0c784..ac5dd7c0ed 100644 --- a/src/Avalonia.Themes.Fluent/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -1,81 +1,176 @@ - + + + + + + + + + - + + + + + + + + - + + + + diff --git a/src/Avalonia.Themes.Fluent/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/ScrollBar.xaml index 0f8fa4986d..4727ff72b9 100644 --- a/src/Avalonia.Themes.Fluent/ScrollBar.xaml +++ b/src/Avalonia.Themes.Fluent/ScrollBar.xaml @@ -1,142 +1,302 @@ - + + + + + - + + - - - - - - + + - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/ScrollViewer.xaml index 1d893133e1..fb73f7eab1 100644 --- a/src/Avalonia.Themes.Fluent/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/ScrollViewer.xaml @@ -1,47 +1,69 @@ - + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/SplitView.xaml b/src/Avalonia.Themes.Fluent/SplitView.xaml new file mode 100644 index 0000000000..71e92459f1 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/SplitView.xaml @@ -0,0 +1,219 @@ + + + + 320 + 48 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index ed172b52ab..88266ac979 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -3,8 +3,8 @@ xmlns:sys="clr-namespace:System;assembly=netstandard"> 0,0,0,6 - 6 - 6 + 6 + 6 154 20 0 diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index cf6f32f9bc..47ad494bbf 100644 --- a/src/Avalonia.Themes.Fluent/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -1,11 +1,14 @@ - + + @@ -19,7 +22,7 @@ @@ -34,6 +37,10 @@ + + 320 + + - + + + + + + + + + + + + + + + - + + 16 + 12 + 12, 0, 12, 0 + M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z + M0,1 L10,10 20,1 19,0 10,8 1,0 Z + + - + - + - + + + - + + + - + + + - + + + + + + + + + + + + + + + + + + + + + - 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/Assets/GraphemeBreak.trie b/src/Avalonia.Visuals/Assets/GraphemeBreak.trie index 704dea4e86..482bf9b44d 100644 Binary files a/src/Avalonia.Visuals/Assets/GraphemeBreak.trie and b/src/Avalonia.Visuals/Assets/GraphemeBreak.trie differ diff --git a/src/Avalonia.Visuals/Assets/UnicodeData.trie b/src/Avalonia.Visuals/Assets/UnicodeData.trie index 2e39745646..f96106a5fa 100644 Binary files a/src/Avalonia.Visuals/Assets/UnicodeData.trie and b/src/Avalonia.Visuals/Assets/UnicodeData.trie differ diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs index 03576a4c40..ad3cc9141b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs @@ -2,6 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum BiDiClass { + LeftToRight, //L ArabicLetter, //AL ArabicNumber, //AN ParagraphSeparator, //B @@ -11,7 +12,6 @@ namespace Avalonia.Media.TextFormatting.Unicode EuropeanSeparator, //ES EuropeanTerminator, //ET FirstStrongIsolate, //FSI - LeftToRight, //L LeftToRightEmbedding, //LRE LeftToRightIsolate, //LRI LeftToRightOverride, //LRO diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs index c13074711e..86d39a4283 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs @@ -4,38 +4,39 @@ namespace Avalonia.Media.TextFormatting.Unicode { private static readonly byte[][] s_breakPairTable = { - new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1}, - new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1}, - new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,1,0,1,1,0,0,4,2,4,0,0,0,0,0,0,1,1,1}, + new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4,4}, + new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1,0}, + new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, + new byte[] {0,4,4,1,1,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0}, }; public static PairBreakType Map(LineBreakClass first, LineBreakClass second) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs index 684baae51f..71e4bce106 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs @@ -2,24 +2,20 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum GraphemeBreakClass { - Control, //CN - CR, //CR - EBase, //EB - EBaseGAZ, //EBG - EModifier, //EM - Extend, //EX - GlueAfterZwj, //GAZ - L, //L - LF, //LF - LV, //LV - LVT, //LVT - Prepend, //PP - RegionalIndicator, //RI - SpacingMark, //SM - T, //T - V, //V - Other, //XX - ZWJ, //ZWJ - ExtendedPictographic + Other, + CR, + LF, + Control, + Extend, + ZWJ, + RegionalIndicator, + Prepend, + SpacingMark, + L, + V, + T, + LV, + LVT, + ExtendedPictographic, } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs index 925706dd4f..8b2e3f41e3 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs @@ -34,10 +34,11 @@ namespace Avalonia.Media.TextFormatting.Unicode EBase, //EB EModifier, //EM ZWJ, //ZWJ + ContingentBreak, //CB + Unknown, //XX Ambiguous, //AI MandatoryBreak, //BK - ContingentBreak, //CB ConditionalJapaneseStarter, //CJ CarriageReturn, //CR LineFeed, //LF @@ -45,6 +46,5 @@ namespace Avalonia.Media.TextFormatting.Unicode ComplexContext, //SA Surrogate, //SG Space, //SP - Unknown, //XX } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index a11c008409..25a32bb1a3 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -95,7 +95,8 @@ namespace Avalonia.Media.TextFormatting.Unicode if (_nextClass.Value == LineBreakClass.MandatoryBreak) { - Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos); + _lastPos = _pos; + Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true); return true; } @@ -108,6 +109,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { case PairBreakType.DI: // Direct break shouldBreak = true; + _lastPos = _pos; break; case PairBreakType.IN: // possible indirect break diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs new file mode 100644 index 0000000000..388a7d257d --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting.Unicode +{ + internal static class PropertyValueAliasHelper + { + private static readonly Dictionary s_scriptToTag = + new Dictionary{ + { Script.Unknown, "Zzzz"}, + { Script.Common, "Zyyy"}, + { Script.Inherited, "Zinh"}, + { Script.Adlam, "Adlm"}, + { Script.CaucasianAlbanian, "Aghb"}, + { Script.Ahom, "Ahom"}, + { Script.Arabic, "Arab"}, + { Script.ImperialAramaic, "Armi"}, + { Script.Armenian, "Armn"}, + { Script.Avestan, "Avst"}, + { Script.Balinese, "Bali"}, + { Script.Bamum, "Bamu"}, + { Script.BassaVah, "Bass"}, + { Script.Batak, "Batk"}, + { Script.Bengali, "Beng"}, + { Script.Bhaiksuki, "Bhks"}, + { Script.Bopomofo, "Bopo"}, + { Script.Brahmi, "Brah"}, + { Script.Braille, "Brai"}, + { Script.Buginese, "Bugi"}, + { Script.Buhid, "Buhd"}, + { Script.Chakma, "Cakm"}, + { Script.CanadianAboriginal, "Cans"}, + { Script.Carian, "Cari"}, + { Script.Cham, "Cham"}, + { Script.Cherokee, "Cher"}, + { Script.Chorasmian, "Chrs"}, + { Script.Coptic, "Copt"}, + { Script.Cypriot, "Cprt"}, + { Script.Cyrillic, "Cyrl"}, + { Script.Devanagari, "Deva"}, + { Script.DivesAkuru, "Diak"}, + { Script.Dogra, "Dogr"}, + { Script.Deseret, "Dsrt"}, + { Script.Duployan, "Dupl"}, + { Script.EgyptianHieroglyphs, "Egyp"}, + { Script.Elbasan, "Elba"}, + { Script.Elymaic, "Elym"}, + { Script.Ethiopic, "Ethi"}, + { Script.Georgian, "Geor"}, + { Script.Glagolitic, "Glag"}, + { Script.GunjalaGondi, "Gong"}, + { Script.MasaramGondi, "Gonm"}, + { Script.Gothic, "Goth"}, + { Script.Grantha, "Gran"}, + { Script.Greek, "Grek"}, + { Script.Gujarati, "Gujr"}, + { Script.Gurmukhi, "Guru"}, + { Script.Hangul, "Hang"}, + { Script.Han, "Hani"}, + { Script.Hanunoo, "Hano"}, + { Script.Hatran, "Hatr"}, + { Script.Hebrew, "Hebr"}, + { Script.Hiragana, "Hira"}, + { Script.AnatolianHieroglyphs, "Hluw"}, + { Script.PahawhHmong, "Hmng"}, + { Script.NyiakengPuachueHmong, "Hmnp"}, + { Script.KatakanaOrHiragana, "Hrkt"}, + { Script.OldHungarian, "Hung"}, + { Script.OldItalic, "Ital"}, + { Script.Javanese, "Java"}, + { Script.KayahLi, "Kali"}, + { Script.Katakana, "Kana"}, + { Script.Kharoshthi, "Khar"}, + { Script.Khmer, "Khmr"}, + { Script.Khojki, "Khoj"}, + { Script.KhitanSmallScript, "Kits"}, + { Script.Kannada, "Knda"}, + { Script.Kaithi, "Kthi"}, + { Script.TaiTham, "Lana"}, + { Script.Lao, "Laoo"}, + { Script.Latin, "Latn"}, + { Script.Lepcha, "Lepc"}, + { Script.Limbu, "Limb"}, + { Script.LinearA, "Lina"}, + { Script.LinearB, "Linb"}, + { Script.Lisu, "Lisu"}, + { Script.Lycian, "Lyci"}, + { Script.Lydian, "Lydi"}, + { Script.Mahajani, "Mahj"}, + { Script.Makasar, "Maka"}, + { Script.Mandaic, "Mand"}, + { Script.Manichaean, "Mani"}, + { Script.Marchen, "Marc"}, + { Script.Medefaidrin, "Medf"}, + { Script.MendeKikakui, "Mend"}, + { Script.MeroiticCursive, "Merc"}, + { Script.MeroiticHieroglyphs, "Mero"}, + { Script.Malayalam, "Mlym"}, + { Script.Modi, "Modi"}, + { Script.Mongolian, "Mong"}, + { Script.Mro, "Mroo"}, + { Script.MeeteiMayek, "Mtei"}, + { Script.Multani, "Mult"}, + { Script.Myanmar, "Mymr"}, + { Script.Nandinagari, "Nand"}, + { Script.OldNorthArabian, "Narb"}, + { Script.Nabataean, "Nbat"}, + { Script.Newa, "Newa"}, + { Script.Nko, "Nkoo"}, + { Script.Nushu, "Nshu"}, + { Script.Ogham, "Ogam"}, + { Script.OlChiki, "Olck"}, + { Script.OldTurkic, "Orkh"}, + { Script.Oriya, "Orya"}, + { Script.Osage, "Osge"}, + { Script.Osmanya, "Osma"}, + { Script.Palmyrene, "Palm"}, + { Script.PauCinHau, "Pauc"}, + { Script.OldPermic, "Perm"}, + { Script.PhagsPa, "Phag"}, + { Script.InscriptionalPahlavi, "Phli"}, + { Script.PsalterPahlavi, "Phlp"}, + { Script.Phoenician, "Phnx"}, + { Script.Miao, "Plrd"}, + { Script.InscriptionalParthian, "Prti"}, + { Script.Rejang, "Rjng"}, + { Script.HanifiRohingya, "Rohg"}, + { Script.Runic, "Runr"}, + { Script.Samaritan, "Samr"}, + { Script.OldSouthArabian, "Sarb"}, + { Script.Saurashtra, "Saur"}, + { Script.SignWriting, "Sgnw"}, + { Script.Shavian, "Shaw"}, + { Script.Sharada, "Shrd"}, + { Script.Siddham, "Sidd"}, + { Script.Khudawadi, "Sind"}, + { Script.Sinhala, "Sinh"}, + { Script.Sogdian, "Sogd"}, + { Script.OldSogdian, "Sogo"}, + { Script.SoraSompeng, "Sora"}, + { Script.Soyombo, "Soyo"}, + { Script.Sundanese, "Sund"}, + { Script.SylotiNagri, "Sylo"}, + { Script.Syriac, "Syrc"}, + { Script.Tagbanwa, "Tagb"}, + { Script.Takri, "Takr"}, + { Script.TaiLe, "Tale"}, + { Script.NewTaiLue, "Talu"}, + { Script.Tamil, "Taml"}, + { Script.Tangut, "Tang"}, + { Script.TaiViet, "Tavt"}, + { Script.Telugu, "Telu"}, + { Script.Tifinagh, "Tfng"}, + { Script.Tagalog, "Tglg"}, + { Script.Thaana, "Thaa"}, + { Script.Thai, "Thai"}, + { Script.Tibetan, "Tibt"}, + { Script.Tirhuta, "Tirh"}, + { Script.Ugaritic, "Ugar"}, + { Script.Vai, "Vaii"}, + { Script.WarangCiti, "Wara"}, + { Script.Wancho, "Wcho"}, + { Script.OldPersian, "Xpeo"}, + { Script.Cuneiform, "Xsux"}, + { Script.Yezidi, "Yezi"}, + { Script.Yi, "Yiii"}, + { Script.ZanabazarSquare, "Zanb"}, + }; + + public static string GetTag(Script script) + { + if(!s_scriptToTag.ContainsKey(script)) + { + return "Zzzz"; + } + return s_scriptToTag[script]; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs index e9681d4c24..2593a77848 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs @@ -2,6 +2,9 @@ namespace Avalonia.Media.TextFormatting.Unicode { public enum Script { + Unknown, //Zzzz + Common, //Zyyy + Inherited, //Zinh Adlam, //Adlm CaucasianAlbanian, //Aghb Ahom, //Ahom @@ -25,10 +28,12 @@ namespace Avalonia.Media.TextFormatting.Unicode Carian, //Cari Cham, //Cham Cherokee, //Cher + Chorasmian, //Chrs Coptic, //Copt Cypriot, //Cprt Cyrillic, //Cyrl Devanagari, //Deva + DivesAkuru, //Diak Dogra, //Dogr Deseret, //Dsrt Duployan, //Dupl @@ -63,6 +68,7 @@ namespace Avalonia.Media.TextFormatting.Unicode Kharoshthi, //Khar Khmer, //Khmr Khojki, //Khoj + KhitanSmallScript, //Kits Kannada, //Knda Kaithi, //Kthi TaiTham, //Lana @@ -151,10 +157,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Wancho, //Wcho OldPersian, //Xpeo Cuneiform, //Xsux + Yezidi, //Yezi Yi, //Yiii ZanabazarSquare, //Zanb - Inherited, //Zinh - Common, //Zyyy - Unknown, //Zzzz } } diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index d1110e0613..d2a72db6ae 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -211,6 +211,21 @@ namespace Avalonia rect.Width * scale.X, rect.Height * scale.Y); } + + /// + /// Multiplies a rectangle by a scale. + /// + /// The rectangle. + /// The scale. + /// The scaled rectangle. + public static Rect operator *(Rect rect, double scale) + { + return new Rect( + rect.X * scale, + rect.Y * scale, + rect.Width * scale, + rect.Height * scale); + } /// /// Divides a rectangle by a vector. 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..23fb27f72b --- /dev/null +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -0,0 +1,198 @@ +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 HideWithSize(Size size) + { + if(_attachedTo == null || _child == null) + return; + if (_mapped) + { + _mapped = false; + XUnmapWindow(_display, _holder.Handle); + } + + size *= _attachedTo.Window.Scaling; + XResizeWindow(_display, _child.Handle, + Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height)); + } + + + + public void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + bounds *= _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; + } + } + } + } +} 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..d7bb2c037e --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -0,0 +1,205 @@ +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 HideWithSize(Size size) + { + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, + -100, -100, 1, 1, + UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | + UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); + if (_attachedTo == null || _child == null) + return; + size *= _attachedTo.Window.Scaling; + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, + Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height), false); + } + + public unsafe void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + bounds *= _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/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index db4c916052..ba1bfda949 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -5,12 +5,13 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls.Platform; +using Avalonia.Logging; namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly CompositeDisposable _disposables; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -32,10 +33,22 @@ namespace Avalonia.Win32 var allDrives = DriveInfo.GetDrives(); var mountVolInfos = allDrives - .Where(p => p.IsReady) + .Where(p => + { + try + { + var ret = p.IsReady; + return ret; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, $"Error in Windows drive enumeration: {e.Message}"); + } + return false; + }) .Select(p => new MountedVolumeInfo() { - VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName + VolumeLabel = string.IsNullOrEmpty(p.VolumeLabel.Trim()) ? p.RootDirectory.FullName : $"{p.VolumeLabel} ({p.Name})", VolumePath = p.RootDirectory.FullName, VolumeSizeBytes = (ulong)p.TotalSize 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.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index df7c0693e1..fa2ed61e65 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Animation.Easings; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Styling; @@ -25,6 +26,16 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(4, keySpline.ControlPointY2); } + [Theory] + [InlineData("1,2F,3,4")] + [InlineData("Foo,Bar,Fee,Buzz")] + public void Can_Handle_Invalid_String_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + Assert.ThrowsAny(() => (KeySpline)conv.ConvertFrom(input)); + } + [Theory] [InlineData(0.00)] [InlineData(0.50)] @@ -46,6 +57,22 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX2 = input); } + [Fact] + public void SplineEasing_Can_Be_Mutated() + { + var easing = new SplineEasing(); + + Assert.Equal(0, easing.Ease(0)); + Assert.Equal(1, easing.Ease(1)); + + easing.X1 = 0.25; + easing.Y1 = 0.5; + easing.X2 = 0.75; + easing.Y2 = 1.0; + + Assert.NotEqual(0.5, easing.Ease(0.5)); + } + /* To get the test values for the KeySpline test, you can: 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations @@ -141,5 +168,73 @@ namespace Avalonia.Animation.UnitTests expected = 1.8016358493761722; Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); } + + [Fact] + public void Check_KeySpline_Parsing_Is_Correct() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, -2.5d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, 2.5d), + }, + KeyTime = TimeSpan.FromSeconds(5), + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(5), + Children = + { + keyframe1, + keyframe2 + }, + IterationCount = new IterationCount(5), + PlaybackDirection = PlaybackDirection.Alternate, + Easing = Easing.Parse("0.1123555056179775,0.657303370786517,0.8370786516853934,0.499999999999999999") + }; + + var rotateTransform = new RotateTransform(-2.5); + var rect = new Rectangle() + { + RenderTransform = rotateTransform + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(rect, clock); + + // position is what you'd expect at end and beginning + clock.Step(TimeSpan.Zero); + Assert.Equal(rotateTransform.Angle, -2.5); + clock.Step(TimeSpan.FromSeconds(5)); + Assert.Equal(rotateTransform.Angle, 2.5); + + // test some points in between end and beginning + var tolerance = 0.01; + clock.Step(TimeSpan.Parse("00:00:10.0153932")); + var expected = -2.4122350198982545; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:11.2655407")); + expected = -0.37153223002125113; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:12.6158773")); + expected = 0.3967885416786294; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:14.6495256")); + expected = 1.8016358493761722; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + } } } 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.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index a12d07b8ef..de3fadd157 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -199,24 +199,5 @@ namespace Avalonia.Base.UnitTests.Utilities var actual = MathUtilities.GreaterThanOrClose(1f, 1f); Assert.True(actual); } - - [Fact] - public void Round_Layout_Value_Without_DPI_Aware() - { - const double value = 42.5; - var expectedValue = Math.Round(value); - var actualValue = MathUtilities.RoundLayoutValue(value, 1.0); - Assert.Equal(expectedValue, actualValue); - } - - [Fact] - public void Round_Layout_Value_With_DPI_Aware() - { - const double dpiScale = 1.25; - const double value = 42.5; - var expectedValue = Math.Round(value * dpiScale) / dpiScale; - var actualValue = MathUtilities.RoundLayoutValue(value, dpiScale); - Assert.Equal(expectedValue, actualValue); - } } } 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/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 57f0230f48..f6130d49d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -117,13 +117,17 @@ namespace Avalonia.Controls.UnitTests.Presenters Width = 150, Height = 150, }, - Offset = new Vector(25, 25), }; target.UpdateChild(); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.Offset = new Vector(25, 25); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); } @@ -345,4 +349,4 @@ namespace Avalonia.Controls.UnitTests.Presenters } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 53591eda5b..a3292644b3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -59,13 +59,17 @@ namespace Avalonia.Controls.UnitTests CanHorizontallyScroll = true, CanVerticallyScroll = true, Content = scrollable, - Offset = new Vector(25, 25), }; target.UpdateChild(); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.Offset = new Vector(25, 25); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds); } 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/SplitViewTests.cs b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs new file mode 100644 index 0000000000..03653ec42c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/SplitViewTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + + public class SplitViewTests + { + [Fact] + public void SplitView_PaneOpening_Should_Fire_Before_PaneOpened() + { + var splitView = new SplitView(); + + bool handledOpening = false; + splitView.PaneOpening += (x, e) => + { + handledOpening = true; + }; + + splitView.PaneOpened += (x, e) => + { + Assert.True(handledOpening); + }; + + splitView.IsPaneOpen = true; + } + + [Fact] + public void SplitView_PaneClosing_Should_Fire_Before_PaneClosed() + { + var splitView = new SplitView(); + splitView.IsPaneOpen = true; + + bool handledClosing = false; + splitView.PaneClosing += (x, e) => + { + handledClosing = true; + }; + + splitView.PaneClosed += (x, e) => + { + Assert.True(handledClosing); + }; + + splitView.IsPaneOpen = false; + } + + [Fact] + public void SplitView_Cancel_Close_Should_Prevent_Pane_From_Closing() + { + var splitView = new SplitView(); + splitView.IsPaneOpen = true; + + splitView.PaneClosing += (x, e) => + { + e.Cancel = true; + }; + + splitView.IsPaneOpen = false; + + Assert.True(splitView.IsPaneOpen); + } + } +} 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/LayoutHelperTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutHelperTests.cs new file mode 100644 index 0000000000..f9187edc42 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutHelperTests.cs @@ -0,0 +1,27 @@ +using System; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutHelperTests + { + [Fact] + public void Round_Layout_Value_Without_DPI_Aware() + { + const double value = 42.5; + var expectedValue = Math.Round(value); + var actualValue = LayoutHelper.RoundLayoutValue(value, 1.0); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void Round_Layout_Value_With_DPI_Aware() + { + const double dpiScale = 1.25; + const double value = 42.5; + var expectedValue = Math.Round(value * dpiScale) / dpiScale; + var actualValue = LayoutHelper.RoundLayoutValue(value, dpiScale); + Assert.Equal(expectedValue, actualValue); + } + } +} 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/Media/TextFormatting/BreakPairTable.txt b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt index 90c1e2cee1..814ce15d0a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BreakPairTable.txt @@ -1,33 +1,34 @@ - OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ -OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ -CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % -PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % -ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ -CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % -H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % -H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % -JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % -JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % -JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % -RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % -EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % -EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % -ZWJ _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ % _ % % _ _ ^ # ^ _ _ _ _ _ _ % % % + OP CL CP QU GL NS EX SY IS PR PO NU AL HL ID IN HY BA BB B2 ZW CM WJ H2 H3 JL JV JT RI EB EM ZWJ CB +OP ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ @ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +CL _ ^ ^ % % ^ ^ ^ ^ % % _ _ _ _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CP _ ^ ^ % % ^ ^ ^ ^ % % % % % _ ^ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +QU ^ ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +GL % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +NS _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +EX _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +SY _ ^ ^ % % % ^ ^ ^ _ _ % _ % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +IS _ ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +PR % ^ ^ % % % ^ ^ ^ _ _ % % % % _ % % _ _ ^ # ^ % % % % % _ % % % _ +PO % ^ ^ % % % ^ ^ ^ _ _ % % % _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +NU % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +AL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +HL % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +ID _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +IN _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +HY _ ^ ^ % _ % ^ ^ ^ _ _ % _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +BA _ ^ ^ % _ % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +BB % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % _ +B2 _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ ^ ^ # ^ _ _ _ _ _ _ _ _ % _ +ZW _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ^ _ _ _ _ _ _ _ _ _ _ _ _ +CM % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +WJ % ^ ^ % % % ^ ^ ^ % % % % % % % % % % % ^ # ^ % % % % % % % % % % +H2 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _ +H3 _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _ +JL _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ % % % % _ _ _ _ % _ +JV _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ % % _ _ _ % _ +JT _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ % _ _ _ % _ +RI _ ^ ^ % % % ^ ^ ^ _ _ _ _ _ _ _ % % _ _ ^ # ^ _ _ _ _ _ % _ _ % _ +EB _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ % % _ +EM _ ^ ^ % % % ^ ^ ^ _ % _ _ _ _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +ZWJ % ^ ^ % % % ^ ^ ^ % % % % % _ % % % _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ +CB _ ^ ^ % % _ ^ ^ ^ _ _ _ _ _ _ _ _ _ _ _ ^ # ^ _ _ _ _ _ _ _ _ % _ diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs index 94ab615130..c074e5a1f1 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGenerator.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using Avalonia.Media.TextFormatting.Unicode; @@ -12,6 +11,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { public static void Execute() { + if (!Directory.Exists("Generated")) + { + Directory.CreateDirectory("Generated"); + } + using (var stream = File.Create("Generated\\GraphemeBreak.trie")) { var trie = GenerateBreakTypeTrie(); @@ -22,48 +26,29 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private static UnicodeTrie GenerateBreakTypeTrie() { - var graphemeBreakClassValues = UnicodeEnumsGenerator.GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)"); - - var graphemeBreakClassMapping = graphemeBreakClassValues.Select(x => x.name).ToList(); - var trieBuilder = new UnicodeTrieBuilder(); - var graphemeBreakData = ReadBreakData( - "https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakProperty.txt"); - - foreach (var (start, end, graphemeBreakType) in graphemeBreakData) - { - if (!graphemeBreakClassMapping.Contains(graphemeBreakType)) - { - continue; - } - - if (start == end) - { - trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - else - { - trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - } + var graphemeBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "auxiliary/GraphemeBreakProperty.txt")); - var emojiBreakData = ReadBreakData("https://unicode.org/Public/emoji/12.0/emoji-data.txt"); + var emojiBreakData = ReadBreakData(Path.Combine(UnicodeDataGenerator.Ucd, "emoji/emoji-data.txt")); - foreach (var (start, end, graphemeBreakType) in emojiBreakData) + foreach (var breakData in new [] { graphemeBreakData, emojiBreakData }) { - if (!graphemeBreakClassMapping.Contains(graphemeBreakType)) + foreach (var (start, end, graphemeBreakType) in breakData) { - continue; - } + if (!Enum.TryParse(graphemeBreakType, out var value)) + { + continue; + } - if (start == end) - { - trieBuilder.Set(start, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); - } - else - { - trieBuilder.SetRange(start, end, (uint)graphemeBreakClassMapping.IndexOf(graphemeBreakType)); + if (start == end) + { + trieBuilder.Set(start, (uint)value); + } + else + { + trieBuilder.SetRange(start, end, (uint)value); + } } } @@ -113,7 +98,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting end = Convert.ToInt32(match.Groups[2].Value, 16); } - data.Add((start, end, match.Groups[3].Value)); + var breakType = match.Groups[3].Value; + + data.Add((start, end, breakType)); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index d9a9c82f85..848b60b341 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -1,9 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using Avalonia.Media.TextFormatting.Unicode; using Xunit; @@ -16,10 +11,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting public class GraphemeBreakClassTrieGeneratorTests { [Theory(Skip = "Only run when we update the trie.")] - [ClassData(typeof(GraphemeEnumeratorTestDataGenerator))] + [ClassData(typeof(GraphemeBreakTestDataGenerator))] public void Should_Enumerate(string text, int expectedLength) { - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var textMemory = text.AsMemory(); + + var enumerator = new GraphemeEnumerator(textMemory); Assert.True(enumerator.MoveNext()); @@ -31,7 +28,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var textMemory = text.AsMemory(); + + var enumerator = new GraphemeEnumerator(textMemory); var count = 0; @@ -51,73 +50,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting GraphemeBreakClassTrieGenerator.Execute(); } - public class GraphemeEnumeratorTestDataGenerator : IEnumerable + private class GraphemeBreakTestDataGenerator : TestDataGenerator { - private readonly List _testData; - - public GraphemeEnumeratorTestDataGenerator() - { - _testData = ReadTestData(); - } - - public IEnumerator GetEnumerator() - { - return _testData.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private static List ReadTestData() + public GraphemeBreakTestDataGenerator() + : base("auxiliary/GraphemeBreakTest.txt") { - var testData = new List(); - - using (var client = new HttpClient()) - { - using (var result = client.GetAsync("https://www.unicode.org/Public/UNIDATA/auxiliary/GraphemeBreakTest.txt").GetAwaiter().GetResult()) - { - if (!result.IsSuccessStatusCode) - return testData; - - using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) - using (var reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - { - var line = reader.ReadLine(); - - if (line == null) - { - break; - } - - if (line.StartsWith("#") || string.IsNullOrEmpty(line)) - { - continue; - } - - var elements = line.Split('#')[0].Replace("÷\t", "÷").Trim('÷').Split('÷'); - - var chars = elements[0].Replace(" × ", " ").Split(' '); - - var codepoints = chars.Where(x => x != "" && x != "×") - .Select(x => Convert.ToInt32(x, 16)).ToArray(); - - var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32)); - - var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum(); - - var data = new object[] { text, length }; - - testData.Add(data); - } - } - } - } - - return testData; } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs index fe7d7adc17..3ed5cfb0b2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs @@ -3,7 +3,7 @@ using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utility; using Xunit; -namespace Avalonia.Visuals.UnitTests.Media.Text +namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { public class LineBreakerTests { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs new file mode 100644 index 0000000000..058de909df --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/TestDataGenerator.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; + +namespace Avalonia.Visuals.UnitTests.Media.TextFormatting +{ + public abstract class TestDataGenerator : IEnumerable + { + private readonly string _fileName; + private readonly List _testData; + + protected TestDataGenerator(string fileName) + { + _fileName = fileName; + _testData = ReadTestData(); + } + + public IEnumerator GetEnumerator() + { + return _testData.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private List ReadTestData() + { + var testData = new List(); + + using (var client = new HttpClient()) + { + var url = Path.Combine(UnicodeDataGenerator.Ucd, _fileName); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) + { + if (!result.IsSuccessStatusCode) + return testData; + + using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + using (var reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + + if (line == null) + { + break; + } + + if (line.StartsWith("#") || string.IsNullOrEmpty(line)) + { + continue; + } + + var elements = line.Split('#'); + + elements = elements[0].Replace("÷\t", "÷").Trim('÷').Split('÷'); + + var chars = elements[0].Replace(" × ", " ").Split(' '); + + var codepoints = chars.Where(x => x != "" && x != "×") + .Select(x => Convert.ToInt32(x, 16)).ToArray(); + + var text = string.Join(null, codepoints.Select(char.ConvertFromUtf32)); + + var length = codepoints.Select(x => x > ushort.MaxValue ? 2 : 1).Sum(); + + var data = new object[] { text, length }; + + testData.Add(data); + } + } + } + } + + return testData; + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs index f7b8b68ab3..cbe8edefb6 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs @@ -9,13 +9,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { internal static class UnicodeDataGenerator { + public const string Ucd = "https://www.unicode.org/Public/13.0.0/ucd/"; + public static void Execute() { var codepoints = new Dictionary(); - var generalCategoryValues = UnicodeEnumsGenerator.CreateGeneralCategoryEnum(); + var generalCategoryEntries = + UnicodeEnumsGenerator.CreateGeneralCategoryEnum(); - var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryValues); + var generalCategoryMappings = CreateTagToIndexMappings(generalCategoryEntries); var generalCategoryData = ReadGeneralCategoryData(); @@ -26,23 +29,23 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddGeneralCategoryRange(codepoints, range, generalCategory); } - var scriptValues = UnicodeEnumsGenerator.CreateScriptEnum(); + var scriptEntries = UnicodeEnumsGenerator.CreateScriptEnum(); - var scriptMappings = CreateNameToIndexMappings(scriptValues); + var scriptMappings = CreateNameToIndexMappings(scriptEntries); var scriptData = ReadScriptData(); foreach (var (range, name) in scriptData) { - var script = scriptMappings[name.Replace("_", "")]; + var script = scriptMappings[name]; AddScriptRange(codepoints, range, script); - } - var biDiClassValues = UnicodeEnumsGenerator.CreateBiDiClassEnum(); + var biDiClassEntries = + UnicodeEnumsGenerator.CreateBiDiClassEnum(); - var biDiClassMappings = CreateTagToIndexMappings(biDiClassValues); + var biDiClassMappings = CreateTagToIndexMappings(biDiClassEntries); var biDiData = ReadBiDiData(); @@ -53,9 +56,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddBiDiClassRange(codepoints, range, biDiClass); } - var lineBreakClassValues = UnicodeEnumsGenerator.CreateLineBreakClassEnum(); + var lineBreakClassEntries = + UnicodeEnumsGenerator.CreateLineBreakClassEnum(); - var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassValues); + var lineBreakClassMappings = CreateTagToIndexMappings(lineBreakClassEntries); var lineBreakClassData = ReadLineBreakClassData(); @@ -66,11 +70,11 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting AddLineBreakClassRange(codepoints, range, lineBreakClass); } - const int initialValue = ((int)LineBreakClass.Unknown << UnicodeData.LINEBREAK_SHIFT) | - ((int)BiDiClass.LeftToRight << UnicodeData.BIDI_SHIFT) | - ((int)Script.Unknown << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other; + //const int initialValue = (0 << UnicodeData.LINEBREAK_SHIFT) | + // (0 << UnicodeData.BIDI_SHIFT) | + // (0 << UnicodeData.SCRIPT_SHIFT) | (int)GeneralCategory.Other; - var builder = new UnicodeTrieBuilder(initialValue); + var builder = new UnicodeTrieBuilder(/*initialValue*/); foreach (var properties in codepoints.Values) { @@ -88,27 +92,30 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting trie.Save(stream); } + + UnicodeEnumsGenerator.CreatePropertyValueAliasHelper(scriptEntries, generalCategoryEntries, + biDiClassEntries, lineBreakClassEntries); } - private static Dictionary CreateTagToIndexMappings(List<(string name, string tag, string comment)> values) + private static Dictionary CreateTagToIndexMappings(List entries) { var mappings = new Dictionary(); - for (var i = 0; i < values.Count; i++) + for (var i = 0; i < entries.Count; i++) { - mappings.Add(values[i].tag, i); + mappings.Add(entries[i].Tag, i); } return mappings; } - private static Dictionary CreateNameToIndexMappings(List<(string name, string tag, string comment)> values) + private static Dictionary CreateNameToIndexMappings(List entries) { var mappings = new Dictionary(); - for (var i = 0; i < values.Count; i++) + for (var i = 0; i < entries.Count; i++) { - mappings.Add(values[i].name, i); + mappings.Add(entries[i].Name, i); } return mappings; @@ -180,24 +187,22 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting public static List<(CodepointRange, string)> ReadGeneralCategoryData() { - return ReadUnicodeData( - "https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedGeneralCategory.txt"); + return ReadUnicodeData("extracted/DerivedGeneralCategory.txt"); } public static List<(CodepointRange, string)> ReadScriptData() { - return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/Scripts.txt"); + return ReadUnicodeData("Scripts.txt"); } public static List<(CodepointRange, string)> ReadBiDiData() { - return ReadUnicodeData("https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedBidiClass.txt"); + return ReadUnicodeData("extracted/DerivedBidiClass.txt"); } public static List<(CodepointRange, string)> ReadLineBreakClassData() { - return ReadUnicodeData( - "https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedLineBreak.txt"); + return ReadUnicodeData("extracted/DerivedLineBreak.txt"); } private static List<(CodepointRange, string)> ReadUnicodeData(string file) @@ -208,7 +213,9 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting using (var client = new HttpClient()) { - using (var result = client.GetAsync(file).GetAwaiter().GetResult()) + var url = Path.Combine(Ucd, file); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) { if (!result.IsSuccessStatusCode) { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs index 47aef84533..5c705ba0c7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeDataGeneratorTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using System; +using Avalonia.Media.TextFormatting.Unicode; +using Xunit; namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { @@ -13,5 +15,26 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { UnicodeDataGenerator.Execute(); } + [Theory(Skip = "Only run when we update the trie.")] + [ClassData(typeof(LineBreakTestDataGenerator))] + + public void Should_Enumerate_LineBreaks(string text, int expectedLength) + { + var textMemory = text.AsMemory(); + + var enumerator = new LineBreakEnumerator(textMemory); + + Assert.True(enumerator.MoveNext()); + + Assert.Equal(expectedLength, enumerator.Current.PositionWrap); + } + + private class LineBreakTestDataGenerator : TestDataGenerator + { + public LineBreakTestDataGenerator() + : base("auxiliary/LineBreakTest.txt") + { + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs index e141204d4c..3a936ff3b0 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/UnicodeEnumsGenerator.cs @@ -8,9 +8,16 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { internal static class UnicodeEnumsGenerator { - public static List<(string name, string tag, string comment)> CreateScriptEnum() + public static List CreateScriptEnum() { - var scriptValues = GetPropertyValueAliases("# Script (sc)"); + var entries = new List + { + new DataEntry("Unknown", "Zzzz", string.Empty), + new DataEntry("Common", "Zyyy", string.Empty), + new DataEntry("Inherited", "Zinh", string.Empty) + }; + + ParseDataEntries("# Script (sc)", entries); using (var stream = File.Create("Generated\\Script.cs")) using (var writer = new StreamWriter(stream)) @@ -20,22 +27,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum Script"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in scriptValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return scriptValues; + return entries; } - public static List<(string name, string tag, string comment)> CreateGeneralCategoryEnum() + public static List CreateGeneralCategoryEnum() { - var generalCategoryValues = GetPropertyValueAliases("# General_Category (gc)"); + var entries = new List { new DataEntry("Other", "C", " Cc | Cf | Cn | Co | Cs") }; + + ParseDataEntries("# General_Category (gc)", entries); using (var stream = File.Create("Generated\\GeneralCategory.cs")) using (var writer = new StreamWriter(stream)) @@ -45,22 +54,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum GeneralCategory"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in generalCategoryValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return generalCategoryValues; + return entries; } - public static List<(string name, string tag, string comment)> CreateGraphemeBreakTypeEnum() + public static List CreateGraphemeBreakTypeEnum() { - var graphemeClusterBreakValues = GetPropertyValueAliases("# Grapheme_Cluster_Break (GCB)"); + var entries = new List { new DataEntry("Other", "XX", string.Empty) }; + + ParseDataEntries("# Grapheme_Cluster_Break (GCB)", entries); using (var stream = File.Create("Generated\\GraphemeBreakClass.cs")) using (var writer = new StreamWriter(stream)) @@ -70,10 +81,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum GraphemeBreakClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in graphemeClusterBreakValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" ExtendedPictographic"); @@ -82,7 +93,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine("}"); } - return graphemeClusterBreakValues; + return entries; } private static List GenerateBreakPairTable() @@ -185,20 +196,32 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting } } - public static List<(string name, string tag, string comment)> CreateLineBreakClassEnum() + public static List CreateLineBreakClassEnum() { var usedLineBreakClasses = GenerateBreakPairTable(); - var lineBreakValues = GetPropertyValueAliases("# Line_Break (lb)"); + var entries = new List { new DataEntry("Unknown", "XX", string.Empty) }; + + ParseDataEntries("# Line_Break (lb)", entries); - var lineBreakClassMappings = lineBreakValues.ToDictionary(x => x.tag, x => (x.name, x.tag, x.comment)); + var orderedLineBreakEntries = new Dictionary(); - var orderedLineBreakValues = usedLineBreakClasses.Select(x => + foreach (var tag in usedLineBreakClasses) { - var value = lineBreakClassMappings[x]; - lineBreakClassMappings.Remove(x); - return value; - }).ToList(); + var entry = entries.Single(x => x.Tag == tag); + + orderedLineBreakEntries.Add(tag, entry); + } + + foreach (var entry in entries) + { + if (orderedLineBreakEntries.ContainsKey(entry.Tag)) + { + continue; + } + + orderedLineBreakEntries.Add(entry.Tag, entry); + } using (var stream = File.Create("Generated\\LineBreakClass.cs")) using (var writer = new StreamWriter(stream)) @@ -208,32 +231,24 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum LineBreakClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in orderedLineBreakValues) - { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); - } - - writer.WriteLine(); - - foreach (var (name, tag, comment) in lineBreakClassMappings.Values) + foreach (var entry in orderedLineBreakEntries.Values) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - orderedLineBreakValues.AddRange(lineBreakClassMappings.Values); - - return orderedLineBreakValues; + return orderedLineBreakEntries.Values.ToList(); } - public static List<(string name, string tag, string comment)> CreateBiDiClassEnum() + public static List CreateBiDiClassEnum() { - var biDiClassValues = GetPropertyValueAliases("# Bidi_Class (bc)"); + var entries = new List { new DataEntry("Left_To_Right", "L", string.Empty) }; + + ParseDataEntries("# Bidi_Class (bc)", entries); using (var stream = File.Create("Generated\\BiDiClass.cs")) using (var writer = new StreamWriter(stream)) @@ -243,23 +258,21 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(" public enum BiDiClass"); writer.WriteLine(" {"); - foreach (var (name, tag, comment) in biDiClassValues) + foreach (var entry in entries) { - writer.WriteLine(" " + name + ", //" + tag + - (string.IsNullOrEmpty(comment) ? string.Empty : "#" + comment)); + writer.WriteLine(" " + entry.Name.Replace("_", "") + ", //" + entry.Tag + + (string.IsNullOrEmpty(entry.Comment) ? string.Empty : "#" + entry.Comment)); } writer.WriteLine(" }"); writer.WriteLine("}"); } - return biDiClassValues; + return entries; } - public static void CreatePropertyValueAliasHelper(List<(string name, string tag, string comment)> scriptValues, - List<(string name, string tag, string comment)> generalCategoryValues, - List<(string name, string tag, string comment)> biDiClassValues, - List<(string name, string tag, string comment)> lineBreakValues) + public static void CreatePropertyValueAliasHelper(List scriptEntries, IEnumerable generalCategoryEntries, + IEnumerable biDiClassEntries, IEnumerable lineBreakClassEntries) { using (var stream = File.Create("Generated\\PropertyValueAliasHelper.cs")) using (var writer = new StreamWriter(stream)) @@ -269,35 +282,35 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine("namespace Avalonia.Media.TextFormatting.Unicode"); writer.WriteLine("{"); - writer.WriteLine(" public static class PropertyValueAliasHelper"); + writer.WriteLine(" internal static class PropertyValueAliasHelper"); writer.WriteLine(" {"); - WritePropertyValueAliasGetTag(writer, scriptValues, "Script", "Zzzz"); + WritePropertyValueAliasGetTag(writer, scriptEntries, "Script", "Zzzz"); - WritePropertyValueAlias(writer, scriptValues, "Script", "Unknown"); + WritePropertyValueAlias(writer, scriptEntries, "Script", "Unknown"); - WritePropertyValueAlias(writer, generalCategoryValues, "GeneralCategory", "Other"); + WritePropertyValueAlias(writer, generalCategoryEntries, "GeneralCategory", "Other"); - WritePropertyValueAlias(writer, biDiClassValues, "BiDiClass", "LeftToRight"); + WritePropertyValueAlias(writer, biDiClassEntries, "BiDiClass", "LeftToRight"); - WritePropertyValueAlias(writer, lineBreakValues, "LineBreakClass", "Unknown"); + WritePropertyValueAlias(writer, lineBreakClassEntries, "LineBreakClass", "Unknown"); writer.WriteLine(" }"); writer.WriteLine("}"); } } - public static List<(string name, string tag, string comment)> GetPropertyValueAliases(string property) + public static void ParseDataEntries(string property, List entries) { - var data = new List<(string name, string tag, string comment)>(); - using (var client = new HttpClient()) { - using (var result = client.GetAsync("https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt").GetAwaiter().GetResult()) + var url = Path.Combine(UnicodeDataGenerator.Ucd, "PropertyValueAliases.txt"); + + using (var result = client.GetAsync(url).GetAwaiter().GetResult()) { if (!result.IsSuccessStatusCode) { - return data; + return; } using (var stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) @@ -337,7 +350,12 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting elements = elements[2].Split('#'); - var name = elements[0].Trim().Replace("_", string.Empty); + var name = elements[0].Trim(); + + if (entries.Any(x => x.Name == name)) + { + continue; + } var comment = string.Empty; @@ -346,24 +364,25 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting comment = elements[1]; } - data.Add((name, tag, comment)); + var entry = new DataEntry(name, tag, comment); + + entries.Add(entry); } } } } - - return data; } - private static void WritePropertyValueAliasGetTag(TextWriter writer, - IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) + private static void WritePropertyValueAliasGetTag(TextWriter writer, IEnumerable entries, + string typeName, string defaultValue) { - writer.WriteLine($" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = "); + writer.WriteLine( + $" private static readonly Dictionary<{typeName}, string> s_{typeName.ToLower()}ToTag = "); writer.WriteLine($" new Dictionary<{typeName}, string>{{"); - foreach (var (name, tag, comment) in values) + foreach (var entry in entries) { - writer.WriteLine($" {{ {typeName}.{name}, \"{tag}\"}},"); + writer.WriteLine($" {{ {typeName}.{entry.Name.Replace("_", "")}, \"{entry.Tag}\"}},"); } writer.WriteLine(" };"); @@ -382,15 +401,15 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(); } - private static void WritePropertyValueAlias(TextWriter writer, - IEnumerable<(string name, string tag, string comment)> values, string typeName, string defaultValue) + private static void WritePropertyValueAlias(TextWriter writer, IEnumerable entries, string typeName, + string defaultValue) { writer.WriteLine($" private static readonly Dictionary s_tagTo{typeName} = "); writer.WriteLine($" new Dictionary{{"); - foreach (var (name, tag, comment) in values) + foreach (var entry in entries) { - writer.WriteLine($" {{ \"{tag}\", {typeName}.{name}}},"); + writer.WriteLine($" {{ \"{entry.Tag}\", {typeName}.{entry.Name.Replace("_", "")}}},"); } writer.WriteLine(" };"); @@ -409,4 +428,18 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting writer.WriteLine(); } } + + public readonly struct DataEntry + { + public DataEntry(string name, string tag, string comment) + { + Name = name; + Tag = tag; + Comment = comment; + } + + public string Name { get; } + public string Tag { get; } + public string Comment { get; } + } } 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);