diff --git a/Avalonia.sln b/Avalonia.sln index e40ebae4d6..810ba5c12f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -204,6 +204,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1895,6 +1897,30 @@ Global {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhone.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhone.Build.0 = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1951,6 +1977,7 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 603308ef9a..accad63faa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,9 +34,17 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.0.x' inputs: - version: '2.1.403' + packageType: sdk + version: 3.0.x + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 2.1.x' + inputs: + packageType: runtime + version: 2.1.x - task: CmdLine@2 displayName: 'Install Mono 5.18' diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index f8767c7599..873048ef21 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 44d5c239ef..897e70ff81 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.999 + 0.9.999 Copyright 2019 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 796bd8e596..08a9aa3ceb 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index f1c7664c3e..677fa2f4c5 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -92,9 +92,17 @@ enum AvnRawMouseEventType RightButtonUp, MiddleButtonDown, MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, Move, Wheel, - NonClientLeftButtonDown + NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd, + TouchCancel }; enum AvnRawKeyEventType @@ -112,7 +120,9 @@ enum AvnInputModifiers Windows = 8, LeftMouseButton = 16, RightMouseButton = 32, - MiddleMouseButton = 64 + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256 }; enum AvnWindowState @@ -212,6 +222,10 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0; virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; + virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; + virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; + virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; virtual bool TryLock() = 0; virtual void Unlock() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 81855995b7..5c50aad4cc 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -1,16 +1,25 @@ #include "common.h" @interface AvnAppDelegate : NSObject @end + extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @implementation AvnAppDelegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { - [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; + if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy) + { + for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) { + [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + break; + } + + [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; + } } - (void)applicationDidFinishLaunching:(NSNotification *)notification { - [NSApp activateIgnoringOtherApps:true]; + [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } @end @@ -20,5 +29,4 @@ extern void InitializeAvnApp() NSApplication* app = [NSApplication sharedApplication]; id delegate = [AvnAppDelegate new]; [app setDelegate:delegate]; - } diff --git a/native/Avalonia.Native/src/OSX/gl.mm b/native/Avalonia.Native/src/OSX/gl.mm index 083adc927d..feb0643654 100644 --- a/native/Avalonia.Native/src/OSX/gl.mm +++ b/native/Avalonia.Native/src/OSX/gl.mm @@ -1,6 +1,7 @@ #include "common.h" #include #include +#include "window.h" template char (&ArrayCounter(T (&a)[N]))[N]; #define ARRAY_COUNT(a) (sizeof(ArrayCounter(a))) @@ -181,12 +182,12 @@ extern IAvnGlFeature* GetGlFeature() class AvnGlRenderingSession : public ComSingleObject { - NSView* _view; - NSWindow* _window; + AvnView* _view; + AvnWindow* _window; NSOpenGLContext* _context; public: FORWARD_IUNKNOWN() - AvnGlRenderingSession(NSWindow*window, NSView* view, NSOpenGLContext* context) + AvnGlRenderingSession(AvnWindow*window, AvnView* view, NSOpenGLContext* context) { _context = context; _window = window; @@ -195,14 +196,12 @@ public: virtual HRESULT GetPixelSize(AvnPixelSize* ret) override { - auto fsize = [_view convertSizeToBacking: [_view frame].size]; - ret->Width = (int)fsize.width; - ret->Height = (int)fsize.height; + *ret = [_view getPixelSize]; return S_OK; } virtual HRESULT GetScaling(double* ret) override { - *ret = [_window backingScaleFactor]; + *ret = [_window getScaling]; return S_OK; } @@ -234,8 +233,17 @@ public: auto f = GetFeature(); if(f == NULL) return E_FAIL; - if(![_view lockFocusIfCanDraw]) + + @try + { + if(![_view lockFocusIfCanDraw]) + return E_ABORT; + } + @catch(NSException* exception) + { return E_ABORT; + } + auto gl = _context; CGLLockContext([_context CGLContextObj]); diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 932bc56a2e..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) onClosed; +-(AvnPixelSize) getPixelSize; @end @interface AvnWindow : NSWindow @@ -22,6 +23,7 @@ class WindowBaseImpl; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; -(void) applyMenu:(NSMenu *)menu; +-(double) getScaling; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 0e85332555..3acc5e365b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -83,6 +83,54 @@ public: [Window setContentView: View]; } + virtual HRESULT ObtainNSWindowHandle(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge void*)Window; + + return S_OK; + } + + virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge_retained void*)Window; + + return S_OK; + } + + virtual HRESULT ObtainNSViewHandle(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge void*)View; + + return S_OK; + } + + virtual HRESULT ObtainNSViewHandleRetained(void** ret) override + { + if (ret == nullptr) + { + return E_POINTER; + } + + *ret = (__bridge_retained void*)View; + + return S_OK; + } + virtual AvnWindow* GetNSWindow() override { return Window; @@ -147,7 +195,11 @@ public: { @autoreleasepool { - [Window close]; + if (Window != nullptr) + { + [Window close]; + } + return S_OK; } } @@ -243,7 +295,14 @@ public: { @autoreleasepool { - return [View lockFocusIfCanDraw] == YES; + @try + { + return [View lockFocusIfCanDraw] == YES; + } + @catch (NSException*) + { + return NO; + } } } @@ -668,18 +727,36 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent AvnFramebuffer _swRenderedFrameBuffer; bool _queuedDisplayFromThread; NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isMouseOver; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; } -- (void)dealloc +- (void)onClosed { + @synchronized (self) + { + _parent = nullptr; + } } -- (void)onClosed +- (BOOL)lockFocusIfCanDraw { - _parent = NULL; + @synchronized (self) + { + if(_parent == nullptr) + { + return NO; + } + } + + return [super lockFocusIfCanDraw]; +} + +-(AvnPixelSize) getPixelSize +{ + return _lastPixelSize; } - (NSEvent*) lastMouseDownEvent @@ -691,8 +768,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { self = [super init]; [self setWantsBestResolutionOpenGLSurface:true]; + [self setWantsLayer:YES]; _parent = parent; _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; return self; } @@ -734,6 +814,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self addTrackingArea:_area]; _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); } @@ -763,7 +847,13 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)drawRect:(NSRect)dirtyRect { + if (_parent == nullptr) + { + return; + } + _parent->BaseEvents->RunRenderPriorityJobs(); + @synchronized (self) { if(_swRenderedFrame != NULL) { @@ -830,7 +920,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) viewDidChangeBackingProperties { + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + [super viewDidChangeBackingProperties]; } @@ -893,9 +988,23 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)otherMouseDown:(NSEvent *)event { - _isMiddlePressed = true; _lastMouseDownEvent = event; - [self mouseEvent:event withType:MiddleButtonDown]; + + switch(event.buttonNumber) + { + case 3: + _isMiddlePressed = true; + [self mouseEvent:event withType:MiddleButtonDown]; + break; + case 4: + _isXButton1Pressed = true; + [self mouseEvent:event withType:XButton1Down]; + break; + case 5: + _isXButton2Pressed = true; + [self mouseEvent:event withType:XButton2Down]; + break; + } } - (void)rightMouseDown:(NSEvent *)event @@ -913,8 +1022,21 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)otherMouseUp:(NSEvent *)event { - _isMiddlePressed = false; - [self mouseEvent:event withType:MiddleButtonUp]; + switch(event.buttonNumber) + { + case 3: + _isMiddlePressed = false; + [self mouseEvent:event withType:MiddleButtonUp]; + break; + case 4: + _isXButton1Pressed = false; + [self mouseEvent:event withType:XButton1Up]; + break; + case 5: + _isXButton2Pressed = false; + [self mouseEvent:event withType:XButton2Up]; + break; + } } - (void)rightMouseUp:(NSEvent *)event @@ -1013,6 +1135,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent rv |= MiddleMouseButton; if (_isRightPressed) rv |= RightMouseButton; + if (_isXButton1Pressed) + rv |= XButton1MouseButton; + if (_isXButton2Pressed) + rv |= XButton2MouseButton; return (AvnInputModifiers)rv; } @@ -1081,6 +1207,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _closed; NSMenu* _menu; bool _isAppMenuApplied; + double _lastScaling; +} + +-(double) getScaling +{ + return _lastScaling; } +(void)closeAll @@ -1094,10 +1226,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -- (void)dealloc -{ -} - - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; @@ -1152,6 +1280,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setReleasedWhenClosed:false]; _parent = parent; [self setDelegate:self]; + _closed = false; + + _lastScaling = [self backingScaleFactor]; + [self setOpaque:NO]; + [self setBackgroundColor: [NSColor clearColor]]; + [self invalidateShadow]; return self; } @@ -1167,6 +1301,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return true; } +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + _lastScaling = [self backingScaleFactor]; +} + - (void)windowWillClose:(NSNotification *)notification { _closed = true; @@ -1177,9 +1316,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self restoreParentWindow]; parent->BaseEvents->Closed(); [parent->View onClosed]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self setContentView: nil]; - }); } } @@ -1326,18 +1462,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } - -// TODO this breaks resizing. -/*- (void)windowDidResize:(NSNotification *)notification -{ - - auto parent = dynamic_cast(_parent.operator->()); - - if(parent != nullptr) - { - parent->WindowStateChanged(); - } -}*/ @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index dd2f27116d..7b3b8465ce 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -191,6 +191,7 @@ partial class Build : NukeBuild RunCoreTest("./tests/Avalonia.Animation.UnitTests"); RunCoreTest("./tests/Avalonia.Base.UnitTests"); RunCoreTest("./tests/Avalonia.Controls.UnitTests"); + RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); RunCoreTest("./tests/Avalonia.Input.UnitTests"); RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); RunCoreTest("./tests/Avalonia.Layout.UnitTests"); diff --git a/readme.md b/readme.md index 512b35a454..97c6509362 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,7 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). -Avalonia is delivered via NuGet package manager. You can find the packages here: ([stable(ish)](https://www.nuget.org/packages/Avalonia/), [nightly](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed)) +Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) Use these commands in the Package Manager console to install Avalonia manually: ``` diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 958729e2e8..52a9591c94 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,6 +1,4 @@ -using System; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -19,7 +17,7 @@ namespace ControlCatalog desktopLifetime.MainWindow = new MainWindow(); else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); - + base.OnFrameworkInitializationCompleted(); } } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 874560a294..cbe2c62890 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -32,7 +32,11 @@ - + + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 3eb6d5b595..d50b051d9f 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -1,5 +1,7 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace ControlCatalog.Pages { @@ -14,7 +16,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Find("fontComboBox"); - fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies; + fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index b44fac27cb..9b8f8af765 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,45 +1,52 @@ - - Image - Displays an image - - - - No Stretch - - - - - Fill - - + + + Image + Displays an image + - - Uniform - - + + + + Bitmap + + None + Fill + Uniform + UniformToFill + + + - - UniformToFill - - - - - Window Icon as an Image - - - + + Drawing + + None + Fill + Uniform + UniformToFill + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 792b25963e..bbe89d1dfd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,40 +1,41 @@ -using System.IO; -using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Media.Imaging; +using Avalonia.Media; namespace ControlCatalog.Pages { public class ImagePage : UserControl { - private Image iconImage; + private readonly Image _bitmapImage; + private readonly Image _drawingImage; + public ImagePage() { - this.InitializeComponent(); + InitializeComponent(); + _bitmapImage = this.FindControl("bitmapImage"); + _drawingImage = this.FindControl("drawingImage"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - iconImage = this.Get("Icon"); } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + public void BitmapStretchChanged(object sender, SelectionChangedEventArgs e) + { + if (_bitmapImage != null) + { + var comboxBox = (ComboBox)sender; + _bitmapImage.Stretch = (Stretch)comboxBox.SelectedIndex; + } + } + + public void DrawingStretchChanged(object sender, SelectionChangedEventArgs e) { - base.OnAttachedToVisualTree(e); - if (iconImage.Source == null) + if (_drawingImage != null) { - var windowRoot = e.Root as Window; - if (windowRoot != null) - { - using (var stream = new MemoryStream()) - { - windowRoot.Icon.Save(stream); - stream.Seek(0, SeekOrigin.Begin); - iconImage.Source = new Bitmap(stream); - } - } + var comboxBox = (ComboBox)sender; + _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } } diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 7f63e7725f..b17520a466 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -41,6 +41,9 @@ + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml new file mode 100644 index 0000000000..fb3e318a0e --- /dev/null +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs new file mode 100644 index 0000000000..7f15845596 --- /dev/null +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -0,0 +1,80 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Threading; + +namespace RenderDemo.Pages +{ + public class GlyphRunPage : UserControl + { + private DrawingPresenter _drawingPresenter; + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private float _fontSize = 20; + private int _direction = 10; + + public GlyphRunPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + _drawingPresenter = this.FindControl("drawingPresenter"); + + DispatcherTimer.Run(() => + { + UpdateGlyphRun(); + + return true; + }, TimeSpan.FromSeconds(1)); + } + + private void UpdateGlyphRun() + { + var c = (uint)_rand.Next(65, 90); + + if (_fontSize + _direction > 200) + { + _direction = -10; + } + + if (_fontSize + _direction < 20) + { + _direction = 10; + } + + _fontSize += _direction; + + _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + + var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; + + var drawingGroup = new DrawingGroup(); + + var glyphRunDrawing = new GlyphRunDrawing + { + Foreground = Brushes.Black, + GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), + BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale) + }; + + drawingGroup.Children.Add(glyphRunDrawing); + + var geometryDrawing = new GeometryDrawing + { + Pen = new Pen(Brushes.Black), + Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } + }; + + drawingGroup.Children.Add(geometryDrawing); + + _drawingPresenter.Drawing = drawingGroup; + } + } +} diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index 3eb2276c48..f263786ab7 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -39,7 +39,7 @@ namespace RenderDemo.Pages ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } - context.DrawImage(_bitmap, 1, + context.DrawImage(_bitmap, new Rect(0, 0, 200, 200), new Rect(0, 0, 200, 200)); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); diff --git a/scripts/avalonia-rename.ps1 b/scripts/avalonia-rename.ps1 deleted file mode 100644 index c77dffb55d..0000000000 --- a/scripts/avalonia-rename.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -function Get-NewDirectoryName { - param ([System.IO.DirectoryInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.Parent.FullName $name -} - -function Get-NewFileName { - param ([System.IO.FileInfo]$item) - - $name = $item.Name.Replace("perspex", "avalonia") - $name = $name.Replace("Perspex", "Avalonia") - Join-Path $item.DirectoryName $name -} - -function Rename-Contents { - param ([System.IO.FileInfo] $file) - - $extensions = @(".cs",".xaml",".csproj",".sln",".md",".json",".yml",".partial",".ps1",".nuspec",".htm",".html",".gitmodules".".xml",".plist",".targets",".projitems",".shproj",".xib") - - if ($extensions.Contains($file.Extension)) { - $text = [IO.File]::ReadAllText($file.FullName) - $text = $text.Replace("github.com/perspex", "github.com/avaloniaui") - $text = $text.Replace("github.com/Perspex", "github.com/AvaloniaUI") - $text = $text.Replace("perspex", "avalonia") - $text = $text.Replace("Perspex", "Avalonia") - $text = $text.Replace("PERSPEX", "AVALONIA") - [IO.File]::WriteAllText($file.FullName, $text) - } -} - -function Process-Files { - param ([System.IO.DirectoryInfo] $item) - - $dirs = Get-ChildItem -Path $item.FullName -Directory - $files = Get-ChildItem -Path $item.FullName -File - - foreach ($dir in $dirs) { - Process-Files $dir.FullName - } - - foreach ($file in $files) { - Rename-Contents $file - - $renamed = Get-NewFileName $file - - if ($file.FullName -ne $renamed) { - Write-Host git mv $file.FullName $renamed - & git mv $file.FullName $renamed - } - } - - $renamed = Get-NewDirectoryName $item - - if ($item.FullName -ne $renamed) { - Write-Host git mv $item.FullName $renamed - & git mv $item.FullName $renamed - } -} - -& git submodule deinit . -& git clean -xdf -Process-Files . diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index ac2fd5b984..cc1ac8ded6 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -85,8 +85,8 @@ namespace Avalonia.Animation var instance = transition.Apply( this, Clock ?? Avalonia.Animation.Clock.GlobalClock, - oldValue.ValueOrDefault(), - newValue.ValueOrDefault()); + oldValue.GetValueOrDefault(), + newValue.GetValueOrDefault()); _previousTransitions[property] = instance; return; diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index e9cd0686d8..9f57455639 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -63,7 +63,7 @@ namespace Avalonia.Animation public IterationType RepeatType => _type; /// - /// Gets a value that indicates whether the is set to loop. + /// Gets a value that indicates whether the is set to Infinite. /// public bool IsInfinite => _type == IterationType.Infinite; diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index fdb04b6dfc..d1df5fa5e3 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -18,12 +18,14 @@ namespace Avalonia /// The class that is registering the property. /// The property metadata. /// Whether the property inherits its value. + /// A value validation callback. public AttachedProperty( string name, - Type ownerType, + Type ownerType, StyledPropertyMetadata metadata, - bool inherits = false) - : base(name, ownerType, metadata, inherits) + bool inherits = false, + Func validate = null) + : base(name, ownerType, metadata, inherits, validate) { } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e427c64945..ddc3d8d081 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -229,24 +229,6 @@ namespace Avalonia return property.RouteGetValue(this); } - /// - /// Gets a value. - /// - /// The type of the property. - /// The property. - /// The value. - public T GetValue(AvaloniaProperty property) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property switch - { - StyledPropertyBase styled => GetValue(styled), - DirectPropertyBase direct => GetValue(direct), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") - }; - } - /// /// Gets a value. /// @@ -322,33 +304,6 @@ namespace Avalonia property.RouteSetValue(this, value, priority); } - /// - /// Sets a value. - /// - /// The type of the property. - /// The property. - /// The value. - /// The priority of the value. - public void SetValue( - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - - switch (property) - { - case StyledPropertyBase styled: - SetValue(styled, value, priority); - break; - case DirectPropertyBase direct: - SetValue(direct, value); - break; - default: - throw new NotSupportedException("Unsupported AvaloniaProperty type."); - } - } - /// /// Sets a value. /// @@ -375,7 +330,7 @@ namespace Avalonia else { throw new NotSupportedException( - "Canot set property to Unset at non-local value priority."); + "Cannot set property to Unset at non-local value priority."); } } else if (!(value is DoNothingType)) @@ -399,52 +354,6 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } - /// - /// Binds a to an observable. - /// - /// The property. - /// The observable. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - public IDisposable Bind( - AvaloniaProperty property, - IObservable> source, - BindingPriority priority = BindingPriority.LocalValue) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); - - return property.RouteBind(this, source, priority); - } - - /// - /// Binds a to an observable. - /// - /// The type of the property. - /// The property. - /// The observable. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - public IDisposable Bind( - AvaloniaProperty property, - IObservable> source, - BindingPriority priority = BindingPriority.LocalValue) - { - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); - - return property switch - { - StyledPropertyBase styled => Bind(styled, source, priority), - DirectPropertyBase direct => Bind(direct, source), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), - }; - } - /// /// Binds a to an observable. /// @@ -531,7 +440,7 @@ namespace Avalonia Optional oldValue, Optional newValue) { - if (property.Inherits && !IsSet(property)) + if (property.Inherits && (_values == null || !_values.IsSet(property))) { RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index cd9b40f94a..aa7a675764 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Reactive.Subjects; -using System.Reflection; using Avalonia.Data; using Avalonia.Utilities; @@ -285,22 +283,25 @@ namespace Avalonia /// Whether the property inherits its value. /// The default binding mode for the property. /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, - Func validate = null) + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits); + var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(typeof(TOwner), result); registry.RegisterAttached(typeof(THost), result); @@ -317,22 +318,27 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. + /// A value validation callback. + /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, Type ownerType, TValue defaultValue = default(TValue), bool inherits = false, - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + Func validate = null, + Func coerce = null) where THost : IAvaloniaObject { Contract.Requires(name != null); var metadata = new StyledPropertyMetadata( defaultValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); - var result = new AttachedProperty(name, ownerType, metadata, inherits); + var result = new AttachedProperty(name, ownerType, metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(ownerType, result); registry.RegisterAttached(typeof(THost), result); @@ -555,7 +561,7 @@ namespace Avalonia return result; } - currentType = currentType.GetTypeInfo().BaseType; + currentType = currentType.BaseType; } _metadataCache[type] = _defaultMetadata; diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs index 0c7cd87897..d8ac3752b3 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs`1.cs @@ -60,8 +60,8 @@ namespace Avalonia protected override AvaloniaProperty GetProperty() => Property; - protected override object? GetOldValue() => OldValue.ValueOrDefault(AvaloniaProperty.UnsetValue); + protected override object? GetOldValue() => OldValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); - protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue); + protected override object? GetNewValue() => NewValue.GetValueOrDefault(AvaloniaProperty.UnsetValue); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 50ffb51550..29ab10278b 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -224,12 +224,20 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(name != null); - if (name.Contains('.')) + if (name.Contains(".")) { throw new InvalidOperationException("Attached properties not supported."); } - return GetRegistered(type).FirstOrDefault(x => x.Name == name); + foreach (AvaloniaProperty x in GetRegistered(type)) + { + if (x.Name == name) + { + return x; + } + } + + return null; } /// diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 3c8d4ca7e6..ca51822eed 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -327,6 +327,8 @@ namespace Avalonia.Collections } else { + EnsureCapacity(_inner.Count + list.Count); + using (IEnumerator en = items.GetEnumerator()) { int insertIndex = index; @@ -550,6 +552,24 @@ namespace Avalonia.Collections /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); + private void EnsureCapacity(int capacity) + { + // Adapted from List implementation. + var currentCapacity = _inner.Capacity; + + if (currentCapacity < capacity) + { + var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2; + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + _inner.Capacity = newCapacity; + } + } + /// /// Raises the event with an add action. /// diff --git a/src/Avalonia.Base/Collections/Pooled/ClearMode.cs b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs new file mode 100644 index 0000000000..d78ac8feab --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ClearMode.cs @@ -0,0 +1,40 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +namespace Avalonia.Collections.Pooled +{ + /// + /// This enum allows control over how data is treated when internal + /// arrays are returned to the ArrayPool. Be careful to understand + /// what each option does before using anything other than the default + /// of Auto. + /// + public enum ClearMode + { + /// + /// Auto has different behavior depending on the host project's target framework. + /// .NET Core 2.1: Reference types and value types that contain reference types are cleared + /// when the internal arrays are returned to the pool. Value types that do not contain reference + /// types are not cleared when returned to the pool. + /// .NET Standard 2.0: All user types are cleared before returning to the pool, in case they + /// contain reference types. + /// For .NET Standard, Auto and Always have the same behavior. + /// + Auto = 0, + /// + /// The Always setting has the effect of always clearing user types before returning to the pool. + /// This is the default behavior on .NET Standard.You might want to turn this on in a .NET Core project + /// if you were concerned about sensitive data stored in value types leaking to other pars of your application. + /// + Always = 1, + /// + /// Never will cause pooled collections to never clear user types before returning them to the pool. + /// You might want to use this setting in a .NET Standard project when you know that a particular collection stores + /// only value types and you want the performance benefit of not taking time to reset array items to their default value. + /// Be careful with this setting: if used for a collection that contains reference types, or value types that contain + /// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances + /// that are still being referenced by arrays sitting in the ArrayPool. + /// + Never = 2 + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs new file mode 100644 index 0000000000..2b15388a13 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class ICollectionDebugView + { + private readonly ICollection _collection; + + public ICollectionDebugView(ICollection collection) + { + _collection = collection ?? throw new ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + T[] items = new T[_collection.Count]; + _collection.CopyTo(items, 0); + return items; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs new file mode 100644 index 0000000000..9bc3609dc5 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -0,0 +1,21 @@ +// This source file is adapted from the Collections.Pooled. +// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/) + +using System; +using System.Collections.Generic; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Represents a read-only collection of pooled elements that can be accessed by index + /// + /// The type of elements in the read-only pooled list. + + public interface IReadOnlyPooledList : IReadOnlyList + { + /// + /// Gets a for the items currently in the collection. + /// + ReadOnlySpan Span { get; } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs new file mode 100644 index 0000000000..f0d6b292cc --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -0,0 +1,1531 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// Implements a variable-size list that uses a pooled array to store the + /// elements. A PooledList has a capacity, which is the allocated length + /// of the internal array. As elements are added to a PooledList, the capacity + /// of the PooledList is automatically increased as required by reallocating the + /// internal array. + /// + /// + /// This class is based on the code for but it supports + /// and uses when allocating internal arrays. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(ICollectionDebugView<>))] + [Serializable] + public class PooledList : IList, IReadOnlyPooledList, IList, IDisposable, IDeserializationCallback + { + // internal constant copied from Array.MaxArrayLength + private const int MaxArrayLength = 0x7FEFFFFF; + private const int DefaultCapacity = 4; + private static readonly T[] s_emptyArray = Array.Empty(); + + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _items; // Do not rename (binary serialization) + private int _size; // Do not rename (binary serialization) + private int _version; // Do not rename (binary serialization) + private readonly bool _clearOnFree; + + #region Constructors + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList. The list is initially empty and has a capacity + /// of zero. Upon adding the first element to the list the capacity is + /// increased to DefaultCapacity, and then increased in multiples of two + /// as required. + /// + public PooledList(ClearMode clearMode, ArrayPool customPool) + { + _items = s_emptyArray; + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, bool sizeToCapacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool.Shared, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ArrayPool customPool, bool sizeToCapacity) : this(capacity, ClearMode.Auto, customPool, sizeToCapacity) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool) : this(capacity, clearMode, customPool, false) { } + + /// + /// Constructs a List with a given initial capacity. The list is + /// initially empty, but will have room for the given number of elements + /// before any reallocations are required. + /// + /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. + public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) + { + if (capacity < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + if (capacity == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(capacity); + } + + if (sizeToCapacity) + { + _size = capacity; + if (clearMode != ClearMode.Never) + { + Array.Clear(_items, 0, _size); + } + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + int count = span.Length; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + span.CopyTo(_items); + _size = count; + } + } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection) : this(collection, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode) : this(collection, clearMode, ArrayPool.Shared) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ArrayPool customPool) : this(collection, ClearMode.Auto, customPool) { } + + /// + /// Constructs a PooledList, copying the contents of the given collection. The + /// size and capacity of the new list will both be equal to the size of the + /// given collection. + /// + public PooledList(IEnumerable collection, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count == 0) + { + _items = s_emptyArray; + } + else + { + _items = _pool.Rent(count); + c.CopyTo(_items, 0); + _size = count; + } + break; + + default: + _size = 0; + _items = s_emptyArray; + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + Add(en.Current); + } + break; + } + } + + #endregion + + /// + /// Gets a for the items currently in the collection. + /// + public Span Span => _items.AsSpan(0, _size); + + /// + ReadOnlySpan IReadOnlyPooledList.Span => Span; + + /// + /// Gets and sets the capacity of this list. The capacity is the size of + /// the internal array used to hold items. When set, the internal + /// Memory of the list is reallocated to the given capacity. + /// Note that the return value for this property may be larger than the property was set to. + /// + public int Capacity + { + get => _items.Length; + set + { + if (value < _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); + } + + if (value != _items.Length) + { + if (value > 0) + { + var newItems = _pool.Rent(value); + if (_size > 0) + { + Array.Copy(_items, newItems, _size); + } + ReturnArray(); + _items = newItems; + } + else + { + ReturnArray(); + _size = 0; + } + } + } + } + + /// + /// Read-only property describing how many elements are in the List. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool IList.IsFixedSize => false; + + bool ICollection.IsReadOnly => false; + + bool IList.IsReadOnly => false; + + int ICollection.Count => _size; + + bool ICollection.IsSynchronized => false; + + // Synchronization root for this object. + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Gets or sets the element at the given index. + /// + public T this[int index] + { + get + { + // Following trick can reduce the range check by one + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + return _items[index]; + } + + set + { + if ((uint)index >= (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + _items[index] = value; + _version++; + } + } + + private static bool IsCompatibleObject(object value) + { + // Non-null values are fine. Only accept nulls if T is a class or Nullable. + // Note that default(T) is not equal to null for value types except when T is Nullable. + return ((value is T) || (value == null && default(T) == null)); + } + + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(value, ExceptionArgument.value); + + try + { + this[index] = (T)value; + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T)); + } + } + } + + /// + /// Adds the given object to the end of this list. The size of the list is + /// increased by one. If required, the capacity of the list is doubled + /// before adding the new element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + _version++; + int size = _size; + if ((uint)size < (uint)_items.Length) + { + _size = size + 1; + _items[size] = item; + } + else + { + AddWithResize(item); + } + } + + // Non-inline from List.Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = _size; + EnsureCapacity(size + 1); + _size = size + 1; + _items[size] = item; + } + + int IList.Add(object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Add((T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + + return Count - 1; + } + + /// + /// Adds the elements of the given collection to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(IEnumerable collection) + => InsertRange(_size, collection); + + /// + /// Adds the elements of the given array to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(T[] array) + => AddRange(array.AsSpan()); + + /// + /// Adds the elements of the given to the end of this list. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. + /// + public void AddRange(ReadOnlySpan span) + { + var newSpan = InsertSpan(_size, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + /// The number of items to add. + public Span AddSpan(int count) + => InsertSpan(_size, count); + + public ReadOnlyCollection AsReadOnly() + => new ReadOnlyCollection(this); + + /// + /// Searches a section of the list for a given element using a binary search + /// algorithm. + /// + /// + /// Elements of the list are compared to the search value using + /// the given IComparer interface. If comparer is null, elements of + /// the list are compared to the search value using the IComparable + /// interface, which in that case must be implemented by all elements of the + /// list and the given search value. This method assumes that the given + /// section of the list is already sorted; if this is not the case, the + /// result will be incorrect. + /// + /// The method returns the index of the given value in the list. If the + /// list does not contain the given value, the method returns a negative + /// integer. The bitwise complement operator (~) can be applied to a + /// negative result to produce the index of the first element (if any) that + /// is larger than the given search value. This is also the index at which + /// the search value should be inserted into the list in order for the list + /// to remain sorted. + /// + public int BinarySearch(int index, int count, T item, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + return Array.BinarySearch(_items, index, count, item, comparer); + } + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item) + => BinarySearch(0, Count, item, null); + + /// + /// Searches the list for a given element using a binary search + /// algorithm. If the item implements + /// then that is used for comparison, otherwise is used. + /// + public int BinarySearch(T item, IComparer comparer) + => BinarySearch(0, Count, item, comparer); + + /// + /// Clears the contents of the PooledList. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + _version++; + int size = _size; + _size = 0; + + if (size > 0 && _clearOnFree) + { + // Clear the elements so that the gc can reclaim the references. + Array.Clear(_items, 0, _size); + } + } + + /// + /// Contains returns true if the specified element is in the List. + /// It does a linear, O(n) search. Equality is determined by calling + /// EqualityComparer{T}.Default.Equals. + /// + public bool Contains(T item) + { + // PERF: IndexOf calls Array.IndexOf, which internally + // calls EqualityComparer.Default.IndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.IndexOf. + + return _size != 0 && IndexOf(item) != -1; + } + + bool IList.Contains(object item) + { + if (IsCompatibleObject(item)) + { + return Contains((T)item); + } + return false; + } + + public PooledList ConvertAll(Func converter) + { + if (converter == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.converter); + } + + var list = new PooledList(_size); + for (int i = 0; i < _size; i++) + { + list._items[i] = converter(_items[i]); + } + list._size = _size; + return list; + } + + /// + /// Copies this list to the given span. + /// + public void CopyTo(Span span) + { + if (span.Length < Count) + throw new ArgumentException("Destination span is shorter than the list to be copied."); + + Span.CopyTo(span); + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_items, 0, array, arrayIndex, _size); + } + + // Copies this List into array, which must be of a + // compatible array type. + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + try + { + // Array.Copy will check for NULL. + Array.Copy(_items, 0, array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Ensures that the capacity of this list is at least the given minimum + /// value. If the current capacity of the list is less than min, the + /// capacity is increased to twice the current capacity or to min, + /// whichever is larger. + /// + private void EnsureCapacity(int min) + { + if (_items.Length < min) + { + int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > MaxArrayLength) + newCapacity = MaxArrayLength; + if (newCapacity < min) + newCapacity = min; + Capacity = newCapacity; + } + } + + public bool Exists(Func match) + => FindIndex(match) != -1; + + public bool TryFind(Func match, out T result) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public PooledList FindAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + var list = new PooledList(); + for (int i = 0; i < _size; i++) + { + if (match(_items[i])) + { + list.Add(_items[i]); + } + } + return list; + } + + public int FindIndex(Func match) + => FindIndex(0, _size, match); + + public int FindIndex(int startIndex, Func match) + => FindIndex(startIndex, _size - startIndex, match); + + public int FindIndex(int startIndex, int count, Func match) + { + if ((uint)startIndex > (uint)_size) + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + + if (count < 0 || startIndex > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + if (match is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) + { + if (match(_items[i])) + return i; + } + return -1; + } + + public bool TryFindLast(Func match, out T result) + { + if (match is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = _size - 1; i >= 0; i--) + { + if (match(_items[i])) + { + result = _items[i]; + return true; + } + } + + result = default; + return false; + } + + public int FindLastIndex(Func match) + => FindLastIndex(_size - 1, _size, match); + + public int FindLastIndex(int startIndex, Func match) + => FindLastIndex(startIndex, startIndex + 1, match); + + public int FindLastIndex(int startIndex, int count, Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + if (_size == 0) + { + // Special case for 0 length List + if (startIndex != -1) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + else + { + // Make sure we're not out of range + if ((uint)startIndex >= (uint)_size) + { + ThrowHelper.ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index(); + } + } + + // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0. + if (count < 0 || startIndex - count + 1 < 0) + { + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + } + + int endIndex = startIndex - count; + for (int i = startIndex; i > endIndex; i--) + { + if (match(_items[i])) + { + return i; + } + } + return -1; + } + + public void ForEach(Action action) + { + if (action == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action); + } + + int version = _version; + for (int i = 0; i < _size; i++) + { + if (version != _version) + { + break; + } + action(_items[i]); + } + + if (version != _version) + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + /// + /// Returns an enumerator for this list with the given + /// permission for removal of elements. If modifications made to the list + /// while an enumeration is in progress, the MoveNext and + /// GetObject methods of the enumerator will throw an exception. + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + /// + /// Equivalent to PooledList.Span.Slice(index, count). + /// + public Span GetRange(int index, int count) + { + if (index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size - index < count) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + return Span.Slice(index, count); + } + + /// + /// Returns the index of the first occurrence of a given value in + /// this list. The list is searched forwards from beginning to end. + /// + public int IndexOf(T item) + => Array.IndexOf(_items, item, 0, _size); + + int IList.IndexOf(object item) + { + if (IsCompatibleObject(item)) + { + return IndexOf((T)item); + } + return -1; + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and ending at count number of elements. + /// + public int IndexOf(T item, int index) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return Array.IndexOf(_items, item, index, _size - index); + } + + /// + /// Returns the index of the first occurrence of a given value in a range of + /// this list. The list is searched forwards, starting at index + /// index and upto count number of elements. + /// + public int IndexOf(T item, int index, int count) + { + if (index > _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + if (count < 0 || index > _size - count) + ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count(); + + return Array.IndexOf(_items, item, index, count); + } + + /// + /// Inserts an element into this list at a given index. The size of the list + /// is increased by one. If required, the capacity of the list is doubled + /// before inserting the new element. + /// + public void Insert(int index, T item) + { + // Note that insertions at the end are legal. + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); + } + + if (_size == _items.Length) + EnsureCapacity(_size + 1); + if (index < _size) + { + Array.Copy(_items, index, _items, index + 1, _size - index); + } + _items[index] = item; + _size++; + _version++; + } + + void IList.Insert(int index, object item) + { + ThrowHelper.IfNullAndNullsAreIllegalThenThrow(item, ExceptionArgument.item); + + try + { + Insert(index, (T)item); + } + catch (InvalidCastException) + { + ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); + } + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, IEnumerable collection) + { + if ((uint)index > (uint)_size) + { + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + } + + switch (collection) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); + break; + + case ICollection c: + int count = c.Count; + if (count > 0) + { + EnsureCapacity(_size + count); + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + // If we're inserting a List into itself, we want to be able to deal with that. + if (this == c) + { + // Copy first part of _items to insert location + Array.Copy(_items, 0, _items, index, index); + // Copy last part of _items back to inserted location + Array.Copy(_items, index + count, _items, index * 2, _size - index); + } + else + { + c.CopyTo(_items, index); + } + _size += count; + } + break; + + default: + using (var en = collection.GetEnumerator()) + { + while (en.MoveNext()) + { + Insert(index++, en.Current); + } + } + break; + } + + _version++; + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, ReadOnlySpan span) + { + var newSpan = InsertSpan(index, span.Length, false); + span.CopyTo(newSpan); + } + + /// + /// Inserts the elements of the given collection at a given index. If + /// required, the capacity of the list is increased to twice the previous + /// capacity or the new size, whichever is larger. Ranges may be added + /// to the end of the list by setting index to the List's size. + /// + public void InsertRange(int index, T[] array) + { + if (array is null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + InsertRange(index, array.AsSpan()); + } + + /// + /// Advances the by the number of items specified, + /// increasing the capacity if required, then returns a Span representing + /// the set of items to be added, allowing direct writes to that section + /// of the collection. + /// + public Span InsertSpan(int index, int count) + => InsertSpan(index, count, true); + + private Span InsertSpan(int index, int count, bool clearOutput) + { + EnsureCapacity(_size + count); + + if (index < _size) + { + Array.Copy(_items, index, _items, index + count, _size - index); + } + + _size += count; + _version++; + + var output = _items.AsSpan(index, count); + + if (clearOutput && _clearOnFree) + { + output.Clear(); + } + + return output; + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at the end + /// and ending at the first element in the list. + /// + public int LastIndexOf(T item) + { + if (_size == 0) + { // Special case for empty list + return -1; + } + else + { + return LastIndexOf(item, _size - 1, _size); + } + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and ending at the first element in the list. + /// + public int LastIndexOf(T item, int index) + { + if (index >= _size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + return LastIndexOf(item, index, index + 1); + } + + /// + /// Returns the index of the last occurrence of a given value in a range of + /// this list. The list is searched backwards, starting at index + /// index and upto count elements + /// + public int LastIndexOf(T item, int index, int count) + { + if (Count != 0 && index < 0) + { + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + } + + if (Count != 0 && count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_size == 0) + { + // Special case for empty list + return -1; + } + + if (index >= _size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + if (count > index + 1) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_BiggerThanCollection); + } + + return Array.LastIndexOf(_items, item, index, count); + } + + // Removes the element at the given index. The size of the list is + // decreased by one. + public bool Remove(T item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + void IList.Remove(object item) + { + if (IsCompatibleObject(item)) + { + Remove((T)item); + } + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveAll(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_items[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_items[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _items[freeIndex++] = _items[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + /// + /// Removes the element at the given index. The size of the list is + /// decreased by one. + /// + public void RemoveAt(int index) + { + if ((uint)index >= (uint)_size) + ThrowHelper.ThrowArgumentOutOfRange_IndexException(); + + _size--; + if (index < _size) + { + Array.Copy(_items, index + 1, _items, index, _size - index); + } + _version++; + + if (_clearOnFree) + { + // Clear the removed element so that the gc can reclaim the reference. + _items[_size] = default; + } + } + + /// + /// Removes a range of elements from this list. + /// + public void RemoveRange(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 0) + { + _size -= count; + if (index < _size) + { + Array.Copy(_items, index + count, _items, index, _size - index); + } + + _version++; + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_items, _size, count); + } + } + } + + /// + /// Reverses the elements in this list. + /// + public void Reverse() + => Reverse(0, _size); + + /// + /// Reverses the elements in a range of this list. Following a call to this + /// method, an element in the range given by index and count + /// which was previously located at index i will now be located at + /// index index + (index + count - i - 1). + /// + public void Reverse(int index, int count) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Reverse(_items, index, count); + } + _version++; + } + + /// + /// Sorts the elements in this list. Uses the default comparer and + /// Array.Sort. + /// + public void Sort() + => Sort(0, Count, null); + + /// + /// Sorts the elements in this list. Uses Array.Sort with the + /// provided comparer. + /// + /// + public void Sort(IComparer comparer) + => Sort(0, Count, comparer); + + /// + /// Sorts the elements in a section of this list. The sort compares the + /// elements to each other using the given IComparer interface. If + /// comparer is null, the elements are compared to each other using + /// the IComparable interface, which in that case must be implemented by all + /// elements of the list. + /// + /// This method uses the Array.Sort method to sort the elements. + /// + public void Sort(int index, int count, IComparer comparer) + { + if (index < 0) + ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); + + if (count < 0) + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + + if (_size - index < count) + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + + if (count > 1) + { + Array.Sort(_items, index, count, comparer); + } + _version++; + } + + public void Sort(Func comparison) + { + if (comparison == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.comparison); + } + + if (_size > 1) + { + // List uses ArraySortHelper here but since it's an internal class, + // we're creating an IComparer using the comparison function to avoid + // duplicating all that code. + Array.Sort(_items, 0, _size, new Comparer(comparison)); + } + _version++; + } + + /// + /// ToArray returns an array containing the contents of the List. + /// This requires copying the List, which is an O(n) operation. + /// + public T[] ToArray() + { + if (_size == 0) + { + return s_emptyArray; + } + + return Span.ToArray(); + } + + /// + /// Sets the capacity of this list to the size of the list. This method can + /// be used to minimize a list's memory overhead once it is known that no + /// new elements will be added to the list. To completely clear a list and + /// release all memory referenced by the list, execute the following + /// statements: + /// + /// list.Clear(); + /// list.TrimExcess(); + /// + /// + public void TrimExcess() + { + int threshold = (int)(_items.Length * 0.9); + if (_size < threshold) + { + Capacity = _size; + } + } + + public bool TrueForAll(Func match) + { + if (match == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + } + + for (int i = 0; i < _size; i++) + { + if (!match(_items[i])) + { + return false; + } + } + return true; + } + + private void ReturnArray() + { + if (_items.Length == 0) + return; + + try + { + // Clear the elements so that the gc can reclaim the references. + _pool.Return(_items, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + + _items = s_emptyArray; + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + /// + /// Returns the internal buffers to the ArrayPool. + /// + public void Dispose() + { + ReturnArray(); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledLists will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledList _list; + private int _index; + private readonly int _version; + private T _current; + + internal Enumerator(PooledList list) + { + _list = list; + _index = 0; + _version = list._version; + _current = default; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + var localList = _list; + + if (_version == localList._version && ((uint)_index < (uint)localList._size)) + { + _current = localList._items[_index]; + _index++; + return true; + } + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = _list._size + 1; + _current = default; + return false; + } + + public T Current => _current; + + object IEnumerator.Current + { + get + { + if (_index == 0 || _index == _list._size + 1) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen(); + } + return Current; + } + } + + void IEnumerator.Reset() + { + if (_version != _list._version) + { + ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion(); + } + + _index = 0; + _current = default; + } + } + + private readonly struct Comparer : IComparer + { + private readonly Func _comparison; + + public Comparer(Func comparison) + { + _comparison = comparison; + } + + public int Compare(T x, T y) => _comparison(x, y); + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/PooledStack.cs b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs new file mode 100644 index 0000000000..104a7f97e9 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs @@ -0,0 +1,699 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*============================================================================= +** +** +** Purpose: An array implementation of a generic stack. +** +** +=============================================================================*/ + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading; + +namespace Avalonia.Collections.Pooled +{ + /// + /// A simple stack of objects. Internally it is implemented as an array, + /// so Push can be O(n). Pop is O(1). + /// + [DebuggerTypeProxy(typeof(StackDebugView<>))] + [DebuggerDisplay("Count = {Count}")] + [Serializable] + public class PooledStack : IEnumerable, ICollection, IReadOnlyCollection, IDisposable, IDeserializationCallback + { + [NonSerialized] + private ArrayPool _pool; + [NonSerialized] + private object _syncRoot; + + private T[] _array; // Storage for stack elements. Do not rename (binary serialization) + private int _size; // Number of items in the stack. Do not rename (binary serialization) + private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization) + private readonly bool _clearOnFree; + + private const int DefaultCapacity = 4; + + #region Constructors + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack() : this(ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with the default initial capacity. + /// + public PooledStack(ArrayPool customPool) : this(ClearMode.Auto, customPool) { } + + /// + /// Create a stack with the default initial capacity and a custom ArrayPool. + /// + public PooledStack(ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _array = Array.Empty(); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool.Shared) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ArrayPool customPool) : this(capacity, ClearMode.Auto, customPool) { } + + /// + /// Create a stack with a specific initial capacity. The initial capacity + /// must be a non-negative number. + /// + public PooledStack(int capacity, ClearMode clearMode, ArrayPool customPool) + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + _pool = customPool ?? ArrayPool.Shared; + _array = _pool.Rent(capacity); + _clearOnFree = ShouldClear(clearMode); + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable) : this(enumerable, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ArrayPool customPool) : this(enumerable, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(IEnumerable enumerable, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + + switch (enumerable) + { + case null: + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable); + break; + + case ICollection collection: + if (collection.Count == 0) + { + _array = Array.Empty(); + } + else + { + _array = _pool.Rent(collection.Count); + collection.CopyTo(_array, 0); + _size = collection.Count; + } + break; + + default: + using (var list = new PooledList(enumerable)) + { + _array = _pool.Rent(list.Count); + list.Span.CopyTo(_array); + _size = list.Count; + } + break; + } + } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ArrayPool customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(T[] array, ClearMode clearMode, ArrayPool customPool) : this(array.AsSpan(), clearMode, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span) : this(span, ClearMode.Auto, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode) : this(span, clearMode, ArrayPool.Shared) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ArrayPool customPool) : this(span, ClearMode.Auto, customPool) { } + + /// + /// Fills a Stack with the contents of a particular collection. The items are + /// pushed onto the stack in the same order they are read by the enumerator. + /// + public PooledStack(ReadOnlySpan span, ClearMode clearMode, ArrayPool customPool) + { + _pool = customPool ?? ArrayPool.Shared; + _clearOnFree = ShouldClear(clearMode); + _array = _pool.Rent(span.Length); + span.CopyTo(_array); + _size = span.Length; + } + + #endregion + + /// + /// The number of items in the stack. + /// + public int Count => _size; + + /// + /// Returns the ClearMode behavior for the collection, denoting whether values are + /// cleared from internal arrays before returning them to the pool. + /// + public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot + { + get + { + if (_syncRoot == null) + { + Interlocked.CompareExchange(ref _syncRoot, new object(), null); + } + return _syncRoot; + } + } + + /// + /// Removes all Objects from the Stack. + /// + public void Clear() + { + if (_clearOnFree) + { + Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references. + } + _size = 0; + _version++; + } + + /// + /// Compares items using the default equality comparer + /// + public bool Contains(T item) + { + // PERF: Internally Array.LastIndexOf calls + // EqualityComparer.Default.LastIndexOf, which + // is specialized for different types. This + // boosts performance since instead of making a + // virtual method call each iteration of the loop, + // via EqualityComparer.Default.Equals, we + // only make one virtual call to EqualityComparer.LastIndexOf. + + return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1; + } + + /// + /// This method removes all items which match the predicate. + /// The complexity is O(n). + /// + public int RemoveWhere(Func match) + { + if (match == null) + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); + + int freeIndex = 0; // the first free slot in items array + + // Find the first item which needs to be removed. + while (freeIndex < _size && !match(_array[freeIndex])) + freeIndex++; + if (freeIndex >= _size) + return 0; + + int current = freeIndex + 1; + while (current < _size) + { + // Find the first item which needs to be kept. + while (current < _size && match(_array[current])) + current++; + + if (current < _size) + { + // copy item to the free slot. + _array[freeIndex++] = _array[current++]; + } + } + + if (_clearOnFree) + { + // Clear the removed elements so that the gc can reclaim the references. + Array.Clear(_array, freeIndex, _size - freeIndex); + } + + int result = _size - freeIndex; + _size = freeIndex; + _version++; + return result; + } + + // Copies the stack into an array. + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); + } + + Debug.Assert(array != _array); + int srcIndex = 0; + int dstIndex = arrayIndex + _size; + while (srcIndex < _size) + { + array[--dstIndex] = _array[srcIndex++]; + } + } + + public void CopyTo(Span span) + { + if (span.Length < _size) + { + ThrowHelper.ThrowArgumentException_DestinationTooShort(); + } + + int srcIndex = 0; + int dstIndex = _size; + while (srcIndex < _size) + { + span[--dstIndex] = _array[srcIndex++]; + } + } + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); + } + + if (array.Rank != 1) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported); + } + + if (array.GetLowerBound(0) != 0) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex); + } + + if (array.Length - arrayIndex < _size) + { + ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); + } + + try + { + Array.Copy(_array, 0, array, arrayIndex, _size); + Array.Reverse(array, arrayIndex, _size); + } + catch (ArrayTypeMismatchException) + { + ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType(); + } + } + + /// + /// Returns an IEnumerator for this PooledStack. + /// + /// + public Enumerator GetEnumerator() + => new Enumerator(this); + + /// + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() + => new Enumerator(this); + + public void TrimExcess() + { + if (_size == 0) + { + ReturnArray(replaceWith: Array.Empty()); + _version++; + return; + } + + int threshold = (int)(_array.Length * 0.9); + if (_size < threshold) + { + var newArray = _pool.Rent(_size); + if (newArray.Length < _array.Length) + { + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _version++; + } + else + { + // The array from the pool wasn't any smaller than the one we already had, + // (we can only control minimum size) so return it and do nothing. + // If we create an exact-sized array not from the pool, we'll + // get an exception when returning it to the pool. + _pool.Return(newArray); + } + } + } + + /// + /// Returns the top object on the stack without removing it. If the stack + /// is empty, Peek throws an InvalidOperationException. + /// + public T Peek() + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + return array[size]; + } + + public bool TryPeek(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + result = array[size]; + return true; + } + + /// + /// Pops an item from the top of the stack. If the stack is empty, Pop + /// throws an InvalidOperationException. + /// + public T Pop() + { + int size = _size - 1; + T[] array = _array; + + // if (_size == 0) is equivalent to if (size == -1), and this case + // is covered with (uint)size, thus allowing bounds check elimination + // https://github.com/dotnet/coreclr/pull/9773 + if ((uint)size >= (uint)array.Length) + { + ThrowForEmptyStack(); + } + + _version++; + _size = size; + T item = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return item; + } + + public bool TryPop(out T result) + { + int size = _size - 1; + T[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + _version++; + _size = size; + result = array[size]; + if (_clearOnFree) + { + array[size] = default; // Free memory quicker. + } + return true; + } + + /// + /// Pushes an item to the top of the stack. + /// + public void Push(T item) + { + int size = _size; + T[] array = _array; + + if ((uint)size < (uint)array.Length) + { + array[size] = item; + _version++; + _size = size + 1; + } + else + { + PushWithResize(item); + } + } + + // Non-inline from Stack.Push to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void PushWithResize(T item) + { + var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length); + Array.Copy(_array, newArray, _size); + ReturnArray(replaceWith: newArray); + _array[_size] = item; + _version++; + _size++; + } + + /// + /// Copies the Stack to an array, in the same order Pop would return the items. + /// + public T[] ToArray() + { + if (_size == 0) + return Array.Empty(); + + T[] objArray = new T[_size]; + int i = 0; + while (i < _size) + { + objArray[i] = _array[_size - i - 1]; + i++; + } + return objArray; + } + + private void ThrowForEmptyStack() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Stack was empty."); + } + + private void ReturnArray(T[] replaceWith = null) + { + if (_array?.Length > 0) + { + try + { + _pool.Return(_array, clearArray: _clearOnFree); + } + catch (ArgumentException) + { + // oh well, the array pool didn't like our array + } + } + + if (!(replaceWith is null)) + { + _array = replaceWith; + } + } + + private static bool ShouldClear(ClearMode mode) + { +#if NETCOREAPP2_1 + return mode == ClearMode.Always + || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); +#else + return mode != ClearMode.Never; +#endif + } + + public void Dispose() + { + ReturnArray(replaceWith: Array.Empty()); + _size = 0; + _version++; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + // We can't serialize array pools, so deserialized PooledStacks will + // have to use the shared pool, even if they were using a custom pool + // before serialization. + _pool = ArrayPool.Shared; + } + + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")] + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly PooledStack _stack; + private readonly int _version; + private int _index; + private T _currentElement; + + internal Enumerator(PooledStack stack) + { + _stack = stack; + _version = stack._version; + _index = -2; + _currentElement = default; + } + + public void Dispose() + { + _index = -1; + } + + public bool MoveNext() + { + bool retval; + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + if (_index == -2) + { // First call to enumerator. + _index = _stack._size - 1; + retval = (_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + return retval; + } + if (_index == -1) + { // End of enumeration. + return false; + } + + retval = (--_index >= 0); + if (retval) + _currentElement = _stack._array[_index]; + else + _currentElement = default; + return retval; + } + + public T Current + { + get + { + if (_index < 0) + ThrowEnumerationNotStartedOrEnded(); + return _currentElement; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == -1 || _index == -2); + throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended."); + } + + object IEnumerator.Current + { + get { return Current; } + } + + void IEnumerator.Reset() + { + if (_version != _stack._version) + throw new InvalidOperationException("Collection was modified during enumeration."); + _index = -2; + _currentElement = default; + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs new file mode 100644 index 0000000000..b042388079 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/StackDebugView.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Avalonia.Collections.Pooled +{ + internal sealed class StackDebugView + { + private readonly PooledStack _stack; + + public StackDebugView(PooledStack stack) + { + _stack = stack ?? throw new ArgumentNullException(nameof(stack)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] Items + { + get + { + return _stack.ToArray(); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs new file mode 100644 index 0000000000..74558229c3 --- /dev/null +++ b/src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs @@ -0,0 +1,691 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +// This file defines an internal class used to throw exceptions in BCL code. +// The main purpose is to reduce code size. +// +// The old way to throw an exception generates quite a lot IL code and assembly code. +// Following is an example: +// C# source +// throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key); +// IL code: +// IL_0003: ldstr "key" +// IL_0008: ldstr "ArgumentNull_Key" +// IL_000d: call string System.Environment::GetResourceString(string) +// IL_0012: newobj instance void System.ArgumentNullException::.ctor(string,string) +// IL_0017: throw +// which is 21bytes in IL. +// +// So we want to get rid of the ldstr and call to Environment.GetResource in IL. +// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the +// argument name and resource name in a small integer. The source code will be changed to +// ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key); +// +// The IL code will be 7 bytes. +// IL_0008: ldc.i4.4 +// IL_0009: ldc.i4.4 +// IL_000a: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument) +// IL_000f: ldarg.0 +// +// This will also reduce the Jitted code size a lot. +// +// It is very important we do this for generic classes because we can easily generate the same code +// multiple times for different instantiation. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Avalonia.Collections.Pooled +{ + internal static class ThrowHelper + { + internal static void ThrowArrayTypeMismatchException() + { + throw new ArrayTypeMismatchException(); + } + + internal static void ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + + internal static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException(); + } + + internal static void ThrowArgumentException_DestinationTooShort() + { + throw new ArgumentException("Destination too short."); + } + + internal static void ThrowArgumentException_OverlapAlignmentMismatch() + { + throw new ArgumentException("Overlap alignment mismatch."); + } + + internal static void ThrowArgumentOutOfRange_IndexException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.index, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.value, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.length, + ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); + } + + internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex, + ExceptionResource.ArgumentOutOfRange_Index); + } + + internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count() + { + throw GetArgumentOutOfRangeException(ExceptionArgument.count, + ExceptionResource.ArgumentOutOfRange_Count); + } + + internal static void ThrowWrongKeyTypeArgumentException(T key, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongKeyTypeArgumentException((object)key, targetType); + } + + internal static void ThrowWrongValueTypeArgumentException(T value, Type targetType) + { + // Generic key to move the boxing to the right hand side of throw + throw GetWrongValueTypeArgumentException((object)value, targetType); + } + + private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key) + { + return new ArgumentException($"Error adding duplicate with key: {key}."); + } + + internal static void ThrowAddingDuplicateWithKeyArgumentException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetAddingDuplicateWithKeyArgumentException((object)key); + } + + internal static void ThrowKeyNotFoundException(T key) + { + // Generic key to move the boxing to the right hand side of throw + throw GetKeyNotFoundException((object)key); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw GetArgumentException(resource); + } + + internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + throw GetArgumentException(resource, argument); + } + + private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw GetArgumentNullException(argument); + } + + internal static void ThrowArgumentNullException(ExceptionResource resource) + { + throw new ArgumentNullException(GetResourceString(resource)); + } + + internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource) + { + throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, resource); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + throw GetArgumentOutOfRangeException(argument, paramNumber, resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw GetInvalidOperationException(resource); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e) + { + throw new InvalidOperationException(GetResourceString(resource), e); + } + + internal static void ThrowSerializationException(ExceptionResource resource) + { + throw new SerializationException(GetResourceString(resource)); + } + + internal static void ThrowSecurityException(ExceptionResource resource) + { + throw new System.Security.SecurityException(GetResourceString(resource)); + } + + internal static void ThrowRankException(ExceptionResource resource) + { + throw new RankException(GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException(ExceptionResource resource) + { + throw new NotSupportedException(GetResourceString(resource)); + } + + internal static void ThrowUnauthorizedAccessException(ExceptionResource resource) + { + throw new UnauthorizedAccessException(GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource) + { + throw new ObjectDisposedException(objectName, GetResourceString(resource)); + } + + internal static void ThrowObjectDisposedException(ExceptionResource resource) + { + throw new ObjectDisposedException(null, GetResourceString(resource)); + } + + internal static void ThrowNotSupportedException() + { + throw new NotSupportedException(); + } + + internal static void ThrowAggregateException(List exceptions) + { + throw new AggregateException(exceptions); + } + + internal static void ThrowOutOfMemoryException() + { + throw new OutOfMemoryException(); + } + + internal static void ThrowArgumentException_Argument_InvalidArrayType() + { + throw new ArgumentException("Invalid array type."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() + { + throw new InvalidOperationException("Enumeration has not started."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded() + { + throw new InvalidOperationException("Enumeration has ended."); + } + + internal static void ThrowInvalidOperationException_EnumCurrent(int index) + { + throw GetInvalidOperationException_EnumCurrent(index); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() + { + throw new InvalidOperationException("Collection was modified during enumeration."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen() + { + throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed."); + } + + internal static void ThrowInvalidOperationException_InvalidOperation_NoValue() + { + throw new InvalidOperationException("No value provided."); + } + + internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported() + { + throw new InvalidOperationException("Concurrent operations are not supported."); + } + + internal static void ThrowInvalidOperationException_HandleIsNotInitialized() + { + throw new InvalidOperationException("Handle is not initialized."); + } + + internal static void ThrowFormatException_BadFormatSpecifier() + { + throw new FormatException("Bad format specifier."); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource) + { + return new InvalidOperationException(GetResourceString(resource)); + } + + private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType) + { + return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key)); + } + + private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType) + { + return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value)); + } + + private static KeyNotFoundException GetKeyNotFoundException(object key) + { + return new KeyNotFoundException($"Key not found: {key}"); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource)); + } + + private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument) + { + return new ArgumentException(GetResourceString(resource), GetArgumentName(argument)); + } + + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource)); + } + + private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index) + { + return new InvalidOperationException( + index < 0 ? + "Enumeration has not started" : + "Enumeration has ended"); + } + + // Allow nulls for reference types and Nullable, but not for value types. + // Aggressively inline so the jit evaluates the if in place and either drops the call altogether + // Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void IfNullAndNullsAreIllegalThenThrow(object value, ExceptionArgument argName) + { + // Note that default(T) is not equal to null for value types except when T is Nullable. + if (!(default(T) == null) && value == null) + ThrowHelper.ThrowArgumentNullException(argName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ThrowForUnsupportedVectorBaseType() where T : struct + { + if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) && + typeof(T) != typeof(short) && typeof(T) != typeof(ushort) && + typeof(T) != typeof(int) && typeof(T) != typeof(uint) && + typeof(T) != typeof(long) && typeof(T) != typeof(ulong) && + typeof(T) != typeof(float) && typeof(T) != typeof(double)) + { + ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionArgument enum value to the argument name string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } +#endif + + private static string GetArgumentName(ExceptionArgument argument) + { + switch (argument) + { + case ExceptionArgument.obj: + return "obj"; + case ExceptionArgument.dictionary: + return "dictionary"; + case ExceptionArgument.array: + return "array"; + case ExceptionArgument.info: + return "info"; + case ExceptionArgument.key: + return "key"; + case ExceptionArgument.text: + return "text"; + case ExceptionArgument.values: + return "values"; + case ExceptionArgument.value: + return "value"; + case ExceptionArgument.startIndex: + return "startIndex"; + case ExceptionArgument.task: + return "task"; + case ExceptionArgument.ch: + return "ch"; + case ExceptionArgument.s: + return "s"; + case ExceptionArgument.input: + return "input"; + case ExceptionArgument.list: + return "list"; + case ExceptionArgument.index: + return "index"; + case ExceptionArgument.capacity: + return "capacity"; + case ExceptionArgument.collection: + return "collection"; + case ExceptionArgument.item: + return "item"; + case ExceptionArgument.converter: + return "converter"; + case ExceptionArgument.match: + return "match"; + case ExceptionArgument.count: + return "count"; + case ExceptionArgument.action: + return "action"; + case ExceptionArgument.comparison: + return "comparison"; + case ExceptionArgument.exceptions: + return "exceptions"; + case ExceptionArgument.exception: + return "exception"; + case ExceptionArgument.enumerable: + return "enumerable"; + case ExceptionArgument.start: + return "start"; + case ExceptionArgument.format: + return "format"; + case ExceptionArgument.culture: + return "culture"; + case ExceptionArgument.comparer: + return "comparer"; + case ExceptionArgument.comparable: + return "comparable"; + case ExceptionArgument.source: + return "source"; + case ExceptionArgument.state: + return "state"; + case ExceptionArgument.length: + return "length"; + case ExceptionArgument.comparisonType: + return "comparisonType"; + case ExceptionArgument.manager: + return "manager"; + case ExceptionArgument.sourceBytesToCopy: + return "sourceBytesToCopy"; + case ExceptionArgument.callBack: + return "callBack"; + case ExceptionArgument.creationOptions: + return "creationOptions"; + case ExceptionArgument.function: + return "function"; + case ExceptionArgument.delay: + return "delay"; + case ExceptionArgument.millisecondsDelay: + return "millisecondsDelay"; + case ExceptionArgument.millisecondsTimeout: + return "millisecondsTimeout"; + case ExceptionArgument.timeout: + return "timeout"; + case ExceptionArgument.type: + return "type"; + case ExceptionArgument.sourceIndex: + return "sourceIndex"; + case ExceptionArgument.sourceArray: + return "sourceArray"; + case ExceptionArgument.destinationIndex: + return "destinationIndex"; + case ExceptionArgument.destinationArray: + return "destinationArray"; + case ExceptionArgument.other: + return "other"; + case ExceptionArgument.newSize: + return "newSize"; + case ExceptionArgument.lowerBounds: + return "lowerBounds"; + case ExceptionArgument.lengths: + return "lengths"; + case ExceptionArgument.len: + return "len"; + case ExceptionArgument.keys: + return "keys"; + case ExceptionArgument.indices: + return "indices"; + case ExceptionArgument.endIndex: + return "endIndex"; + case ExceptionArgument.elementType: + return "elementType"; + case ExceptionArgument.arrayIndex: + return "arrayIndex"; + default: + Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum."); + return argument.ToString(); + } + } + +#if false // Reflection-based implementation does not work for CoreRT/ProjectN + // This function will convert an ExceptionResource enum value to the resource string. + [MethodImpl(MethodImplOptions.NoInlining)] + private static string GetResourceString(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return SR.GetResourceString(resource.ToString()); + } +#endif + + private static string GetResourceString(ExceptionResource resource) + { + switch (resource) + { + case ExceptionResource.ArgumentOutOfRange_Index: + return "Argument 'index' was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_Count: + return "Argument 'count' was out of the range of valid values."; + case ExceptionResource.Arg_ArrayPlusOffTooSmall: + return "Array plus offset too small."; + case ExceptionResource.NotSupported_ReadOnlyCollection: + return "This operation is not supported on a read-only collection."; + case ExceptionResource.Arg_RankMultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_NonZeroLowerBound: + return "Arrays with a non-zero lower bound are not supported."; + case ExceptionResource.ArgumentOutOfRange_ListInsert: + return "Insertion index was out of the range of valid values."; + case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum: + return "The number must be non-negative."; + case ExceptionResource.ArgumentOutOfRange_SmallCapacity: + return "The capacity cannot be set below the current Count."; + case ExceptionResource.Argument_InvalidOffLen: + return "Invalid offset length."; + case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection: + return "The given value was larger than the size of the collection."; + case ExceptionResource.Serialization_MissingKeys: + return "Serialization error: missing keys."; + case ExceptionResource.Serialization_NullKey: + return "Serialization error: null key."; + case ExceptionResource.NotSupported_KeyCollectionSet: + return "The KeyCollection does not support modification."; + case ExceptionResource.NotSupported_ValueCollectionSet: + return "The ValueCollection does not support modification."; + case ExceptionResource.InvalidOperation_NullArray: + return "Null arrays are not supported."; + case ExceptionResource.InvalidOperation_HSCapacityOverflow: + return "Set hash capacity overflow. Cannot increase size."; + case ExceptionResource.NotSupported_StringComparison: + return "String comparison not supported."; + case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported: + return "SyncRoot not supported."; + case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength: + return "The other array is not of the correct length."; + case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex: + return "The end index does not come after the start index."; + case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported: + return "Huge arrays are not supported."; + case ExceptionResource.Argument_AddingDuplicate: + return "Duplicate item added."; + case ExceptionResource.Argument_InvalidArgumentForComparison: + return "Invalid argument for comparison."; + case ExceptionResource.Arg_LowerBoundsMustMatch: + return "Array lower bounds must match."; + case ExceptionResource.Arg_MustBeType: + return "Argument must be of type: "; + case ExceptionResource.InvalidOperation_IComparerFailed: + return "IComparer failed."; + case ExceptionResource.NotSupported_FixedSizeCollection: + return "This operation is not suppored on a fixed-size collection."; + case ExceptionResource.Rank_MultiDimNotSupported: + return "Multi-dimensional arrays are not supported."; + case ExceptionResource.Arg_TypeNotSupported: + return "Type not supported."; + default: + Debug.Assert(false, + "The enum value is not defined, please check the ExceptionResource Enum."); + return resource.ToString(); + } + } + } + + // + // The convention for this enum is using the argument name as the enum name + // + internal enum ExceptionArgument + { + obj, + dictionary, + array, + info, + key, + text, + values, + value, + startIndex, + task, + ch, + s, + input, + list, + index, + capacity, + collection, + item, + converter, + match, + count, + action, + comparison, + exceptions, + exception, + enumerable, + start, + format, + culture, + comparer, + comparable, + source, + state, + length, + comparisonType, + manager, + sourceBytesToCopy, + callBack, + creationOptions, + function, + delay, + millisecondsDelay, + millisecondsTimeout, + timeout, + type, + sourceIndex, + sourceArray, + destinationIndex, + destinationArray, + other, + newSize, + lowerBounds, + lengths, + len, + keys, + indices, + endIndex, + elementType, + arrayIndex + } + + // + // The convention for this enum is using the resource name as the enum name + // + internal enum ExceptionResource + { + ArgumentOutOfRange_Index, + ArgumentOutOfRange_Count, + Arg_ArrayPlusOffTooSmall, + NotSupported_ReadOnlyCollection, + Arg_RankMultiDimNotSupported, + Arg_NonZeroLowerBound, + ArgumentOutOfRange_ListInsert, + ArgumentOutOfRange_NeedNonNegNum, + ArgumentOutOfRange_SmallCapacity, + Argument_InvalidOffLen, + ArgumentOutOfRange_BiggerThanCollection, + Serialization_MissingKeys, + Serialization_NullKey, + NotSupported_KeyCollectionSet, + NotSupported_ValueCollectionSet, + InvalidOperation_NullArray, + InvalidOperation_HSCapacityOverflow, + NotSupported_StringComparison, + ConcurrentCollection_SyncRoot_NotSupported, + ArgumentException_OtherNotArrayOfCorrectLength, + ArgumentOutOfRange_EndIndexStartIndex, + ArgumentOutOfRange_HugeArrayNotSupported, + Argument_AddingDuplicate, + Argument_InvalidArgumentForComparison, + Arg_LowerBoundsMustMatch, + Arg_MustBeType, + InvalidOperation_IComparerFailed, + NotSupported_FixedSizeCollection, + Rank_MultiDimNotSupported, + Arg_TypeNotSupported, + } +} diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index a568c062d0..9a2cc1bfde 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -30,6 +30,12 @@ namespace Avalonia.Data /// Represents a binding notification that can be a valid binding value, or a binding or /// data validation error. /// + /// + /// This class is very similar to , but where + /// is used by typed bindings, this class is used to hold binding and data validation errors in + /// untyped bindings. As Avalonia moves towards using typed bindings by default we may want to remove + /// this class. + /// public class BindingNotification { /// diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 653388958a..1b47cc7490 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Reactive; @@ -57,22 +56,37 @@ namespace Avalonia.Data if (source != null) { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + return source .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Take(1) - .Subscribe(x => target.SetValue(property, x, binding.Priority)); + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); } else { target.SetValue(property, binding.Value, binding.Priority); return Disposable.Empty; } + case BindingMode.OneWayToSource: + { + // Perf: Avoid allocating closure in the outer scope. + var bindingCopy = binding; + return Observable.CombineLatest( binding.Observable, target.GetObservable(property), (_, v) => v) - .Subscribe(x => binding.Subject.OnNext(x)); + .Subscribe(x => bindingCopy.Subject.OnNext(x)); + } + default: throw new ArgumentException("Invalid binding mode."); } diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 50e71cf3fc..cecdd33e7b 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -8,6 +8,7 @@ namespace Avalonia.Data /// /// Describes the type of a . /// + [Flags] public enum BindingValueType { /// @@ -135,10 +136,10 @@ namespace Avalonia.Data /// Converts the binding value to an . /// /// - public Optional ToOptional() => HasValue ? new Optional(Value) : default; + public Optional ToOptional() => HasValue ? new Optional(_value) : default; /// - public override string ToString() => HasError ? $"Error: {Error!.Message}" : Value?.ToString() ?? "(null)"; + public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)"; /// /// Converts the value to untyped representation, using , @@ -152,7 +153,7 @@ namespace Avalonia.Data { BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, BindingValueType.DoNothing => BindingOperations.DoNothing, - BindingValueType.Value => Value, + BindingValueType.Value => _value, BindingValueType.BindingError => new BindingNotification(Error, BindingErrorType.Error), BindingValueType.BindingErrorWithFallback => @@ -161,7 +162,7 @@ namespace Avalonia.Data new BindingNotification(Error, BindingErrorType.DataValidationError), BindingValueType.DataValidationErrorWithFallback => new BindingNotification(Error, BindingErrorType.DataValidationError, Value), - _ => throw new NotSupportedException("Invalida BindingValueType."), + _ => throw new NotSupportedException("Invalid BindingValueType."), }; } @@ -185,12 +186,32 @@ namespace Avalonia.Data return new BindingValue(type | BindingValueType.HasValue, value, Error); } + /// + /// Gets the value of the binding value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; + /// /// Gets the value of the binding value if present, otherwise a default value. /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } /// /// Gets the value of the binding value if present, otherwise a default value. @@ -201,10 +222,10 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult ValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? - Value is TResult result ? result : default + _value is TResult result ? result : default : defaultValue; } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 986e2cf012..9eec5d6b2b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -21,6 +21,7 @@ namespace Avalonia.Data.Core private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; + private readonly object _targetNullValue; private readonly BindingPriority _priority; InnerListener _innerListener; WeakReference _value; @@ -51,7 +52,7 @@ namespace Avalonia.Data.Core IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) { } @@ -63,6 +64,9 @@ namespace Avalonia.Data.Core /// /// The value to use when the binding is unable to produce a value. /// + /// + /// The value to use when the binding result is null. + /// /// The value converter to use. /// /// A parameter to pass to . @@ -72,6 +76,7 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, object fallbackValue, + object targetNullValue, IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) @@ -85,6 +90,7 @@ namespace Avalonia.Data.Core Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; _priority = priority; } @@ -196,6 +202,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) + { + return _targetNullValue; + } + if (value == BindingOperations.DoNothing) { return value; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7060fd3451..91a27be634 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core /// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. /// - public static readonly IList PropertyAccessors = + public static readonly List PropertyAccessors = new List { new AvaloniaPropertyAccessorPlugin(), @@ -33,7 +33,7 @@ namespace Avalonia.Data.Core /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList DataValidators = + public static readonly List DataValidators = new List { new DataAnnotationsValidationPlugin(), @@ -45,7 +45,7 @@ namespace Avalonia.Data.Core /// An ordered collection of stream plugins that can be used to customize the behavior /// of the '^' stream binding operator. /// - public static readonly IList StreamHandlers = + public static readonly List StreamHandlers = new List { new TaskStreamPlugin(), diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index ab4a109cc2..fa72235a89 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins { @@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins return false; } - private class Accessor : PropertyAccessorBase + private class Accessor : PropertyAccessorBase, IObserver { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; @@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + _subscription = Instance?.GetObservable(_property).Subscribe(this); } protected override void UnsubscribeCore() @@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins _subscription?.Dispose(); _subscription = null; } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + void IObserver.OnNext(object value) + { + PublishValue(value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 70f53b8b88..45b7207413 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -41,7 +39,17 @@ namespace Avalonia.Data.Core { reference.TryGetTarget(out object target); - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + IPropertyAccessorPlugin plugin = null; + + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + plugin = x; + break; + } + } + var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index 3fbcd356ff..dd952c895c 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -54,23 +54,43 @@ namespace Avalonia.Data public bool Equals(Optional other) => this == other; /// - public override int GetHashCode() => HasValue ? Value!.GetHashCode() : 0; + public override int GetHashCode() => HasValue ? _value?.GetHashCode() ?? 0 : 0; /// /// Casts the value (if any) to an . /// /// The cast optional value. - public Optional ToObject() => HasValue ? new Optional(Value) : default; + public Optional ToObject() => HasValue ? new Optional(_value) : default; /// - public override string ToString() => HasValue ? Value?.ToString() ?? "(null)" : "(empty)"; + public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// The value. + public T GetValueOrDefault() => HasValue ? _value : default; /// /// Gets the value if present, otherwise a default value. /// /// The default value. /// The value. - public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; + public T GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; + + /// + /// Gets the value if present, otherwise the default value. + /// + /// + /// The value if present and of the correct type, `default(TResult)` if the value is + /// not present or of an incorrect type. + /// + public TResult GetValueOrDefault() + { + return HasValue ? + _value is TResult result ? result : default + : default; + } /// /// Gets the value if present, otherwise a default value. @@ -81,10 +101,10 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult ValueOrDefault(TResult defaultValue = default) + public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? - Value is TResult result ? result : default + _value is TResult result ? result : default : defaultValue; } diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 1ccdaa8f0b..fb85ae222c 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -118,7 +118,7 @@ namespace Avalonia /// /// The inheritance child. /// - /// Inheritance children will recieve a call to + /// Inheritance children will receive a call to /// /// when an inheritable property value changes on the parent. /// diff --git a/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs new file mode 100644 index 0000000000..837ea1abcd --- /dev/null +++ b/src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs @@ -0,0 +1,15 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Platform +{ + public interface IMacOSTopLevelPlatformHandle + { + IntPtr NSView { get; } + IntPtr GetNSViewRetained(); + IntPtr NSWindow { get; } + IntPtr GetNSWindowRetained(); + } +} diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index a53dda2aef..09a0f169df 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -6,10 +6,17 @@ using Avalonia.Threading; namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// internal interface IBindingEntry : IPriorityValueEntry, IDisposable { } + /// + /// Stores a binding in a or . + /// + /// The property type. internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> { private readonly IAvaloniaObject _owner; diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index bc75eac4ef..f15f56e32b 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -5,6 +5,11 @@ using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Stores a value with a priority in a or + /// . + /// + /// The property type. internal class ConstantValueEntry : IPriorityValueEntry { public ConstantValueEntry( @@ -19,7 +24,7 @@ namespace Avalonia.PropertyStore public StyledPropertyBase Property { get; } public BindingPriority Priority { get; } - public Optional Value { get; private set; } + public Optional Value { get; } Optional IValue.Value => Value.ToObject(); BindingPriority IValue.ValuePriority => Priority; diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 8e239e03c9..6ed6c2ef52 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -5,6 +5,9 @@ using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// internal interface IPriorityValueEntry : IValue { BindingPriority Priority { get; } @@ -12,6 +15,10 @@ namespace Avalonia.PropertyStore void Reparent(IValueSink sink); } + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. internal interface IPriorityValueEntry : IPriorityValueEntry, IValue { } diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index 7d1eaa337f..0ce7fb8308 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -4,12 +4,19 @@ namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// internal interface IValue { Optional Value { get; } BindingPriority ValuePriority { get; } } + /// + /// Represents an object that can act as an entry in a . + /// + /// The property type. internal interface IValue : IValue { new Optional Value { get; } diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index faccd9e75a..223b0058c1 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -1,10 +1,12 @@ -using System; -using Avalonia.Data; +using Avalonia.Data; #nullable enable namespace Avalonia.PropertyStore { + /// + /// Represents an entity that can receive change notifications in a . + /// internal interface IValueSink { void ValueChanged( diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 067ed7b966..22258390da 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -4,6 +4,11 @@ namespace Avalonia.PropertyStore { + /// + /// Stores a value with local value priority in a or + /// . + /// + /// The property type. internal class LocalValueEntry : IValue { private T _value; diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 60baf9d405..2785dc6840 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -6,6 +6,17 @@ using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Stores a set of prioritized values and bindings in a . + /// + /// The property type. + /// + /// When more than a single value or binding is applied to a property in an + /// , the entry in the is converted into + /// a . This class holds any number of + /// entries (sorted first by priority and then in the order + /// they were added) plus a local value. + /// internal class PriorityValue : IValue, IValueSink { private readonly IAvaloniaObject _owner; diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 9501640626..be044b0559 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -12,6 +12,7 @@ namespace Avalonia.Reactive private readonly AvaloniaProperty _property; private T _value; +#nullable disable public AvaloniaPropertyBindingObservable( IAvaloniaObject target, AvaloniaProperty property) @@ -19,6 +20,7 @@ namespace Avalonia.Reactive _target = new WeakReference(target); _property = property; } +#nullable enable public string Description => $"{_target.GetType().Name}.{_property.Name}"; diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index 100330ed1d..238aba5c96 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -44,7 +44,16 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - var newValue = e.Sender.GetValue(e.Property); + T newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = typed.Sender.GetValue(typed.Property); + } + else + { + newValue = (T)e.Sender.GetValue(e.Property); + } if (!Equals(newValue, _value)) { diff --git a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs index 6871602ad9..6f0d29dd0f 100644 --- a/src/Avalonia.Base/Reactive/BindingValueExtensions.cs +++ b/src/Avalonia.Base/Reactive/BindingValueExtensions.cs @@ -20,13 +20,13 @@ namespace Avalonia.Reactive return new BindingValueSubjectAdapter(source); } - public static IObservable ToUntyped(this IObservable> source) + public static IObservable ToUntyped(this IObservable> source) { source = source ?? throw new ArgumentNullException(nameof(source)); return new UntypedBindingAdapter(source); } - public static ISubject ToUntyped(this ISubject> source) + public static ISubject ToUntyped(this ISubject> source) { source = source ?? throw new ArgumentNullException(nameof(source)); return new UntypedBindingSubjectAdapter(source); diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 41009e4cd3..f5052e5858 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -116,20 +116,33 @@ namespace Avalonia.Reactive { if (Volatile.Read(ref _observers) != null) { - IObserver[] observers; - + IObserver[] observers = null; + IObserver singleObserver = null; lock (this) { if (_observers == null) { return; } - observers = _observers.ToArray(); + if (_observers.Count == 1) + { + singleObserver = _observers[0]; + } + else + { + observers = _observers.ToArray(); + } } - - foreach (var observer in observers) + if (singleObserver != null) { - observer.OnNext(value); + singleObserver.OnNext(value); + } + else + { + foreach (var observer in observers) + { + observer.OnNext(value); + } } } } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index bd6c125776..f96298a298 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -33,7 +33,7 @@ namespace Avalonia /// /// Gets the default value for the property. /// - public TValue DefaultValue => _defaultValue.ValueOrDefault(); + public TValue DefaultValue => _defaultValue.GetValueOrDefault(); /// /// Gets the value coercion callback, if any. @@ -60,18 +60,5 @@ namespace Avalonia } } } - - [DebuggerHidden] - private static Func Cast(Func f) - { - if (f == null) - { - return null; - } - else - { - return (o, v) => f(o, (TValue)v); - } - } } } diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..973b0aa641 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -15,7 +15,7 @@ namespace Avalonia.Utilities { if (IsValidIdentifierStart(r.Peek)) { - return r.TakeWhile(IsValidIdentifierChar); + return r.TakeWhile(c => IsValidIdentifierChar(c)); } else { diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs new file mode 100644 index 0000000000..a1246c57b5 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _readOnlyList; + private int _pos; + + public ImmutableReadOnlyListStructEnumerator(IReadOnlyList readOnlyList) + { + _readOnlyList = readOnlyList; + _pos = -1; + Current = default; + } + + public T Current + { + get; + private set; + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + if (_pos >= _readOnlyList.Count - 1) + { + return false; + } + + Current = _readOnlyList[++_pos]; + + return true; + + } + + public void Reset() + { + _pos = -1; + + Current = default; + } + } +} diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3cf05509d4..d1393a9c0d 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -92,8 +92,7 @@ namespace Avalonia.Utilities /// True if the type accepts null values; otherwise false. public static bool AcceptsNull(Type type) { - var t = type.GetTypeInfo(); - return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>))); + return !type.IsValueType || IsNullableType(type); } /// @@ -119,10 +118,8 @@ namespace Avalonia.Utilities } var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (to.IsAssignableFrom(from)) { result = value; return true; @@ -134,7 +131,7 @@ namespace Avalonia.Utilities return true; } - if (toTypeInfo.IsEnum && from == typeof(string)) + if (to.IsEnum && from == typeof(string)) { if (Enum.IsDefined(to, (string)value)) { @@ -143,7 +140,7 @@ namespace Avalonia.Utilities } } - if (!fromTypeInfo.IsEnum && toTypeInfo.IsEnum) + if (!from.IsEnum && to.IsEnum) { result = null; @@ -154,7 +151,7 @@ namespace Avalonia.Utilities } } - if (fromTypeInfo.IsEnum && IsNumeric(to)) + if (from.IsEnum && IsNumeric(to)) { try { @@ -223,10 +220,8 @@ namespace Avalonia.Utilities } var from = value.GetType(); - var fromTypeInfo = from.GetTypeInfo(); - var toTypeInfo = to.GetTypeInfo(); - if (toTypeInfo.IsAssignableFrom(fromTypeInfo)) + if (to.IsAssignableFrom(from)) { result = value; return true; @@ -307,9 +302,7 @@ namespace Avalonia.Utilities /// The default value. public static object Default(Type type) { - var typeInfo = type.GetTypeInfo(); - - if (typeInfo.IsValueType) + if (type.IsValueType) { return Activator.CreateInstance(type); } @@ -335,9 +328,11 @@ namespace Avalonia.Utilities return false; } - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + Type underlyingType = Nullable.GetUnderlyingType(type); + + if (underlyingType != null) { - return IsNumeric(Nullable.GetUnderlyingType(type)); + return IsNumeric(underlyingType); } else { @@ -352,6 +347,11 @@ namespace Avalonia.Utilities Explicit = 2 } + private static bool IsNullableType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType) { const string implicitName = "op_Implicit"; diff --git a/src/Avalonia.Base/Utilities/ValueSingleOrList.cs b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs new file mode 100644 index 0000000000..dc32cedb76 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// A list like struct optimized for holding zero or one items. + /// + /// The type of items held in the list. + /// + /// Once more than value has been added to this storage it will switch to using internally. + /// + public ref struct ValueSingleOrList + { + private bool _isSingleSet; + + /// + /// Single contained value. Only valid if is set. + /// + public T Single { get; private set; } + + /// + /// List of values. + /// + public List List { get; private set; } + + /// + /// If this struct is backed by a list. + /// + public bool HasList => List != null; + + /// + /// If this struct contains only single value and storage was not promoted to a list. + /// + public bool IsSingle => List == null && _isSingleSet; + + /// + /// Adds a value. + /// + /// Value to add. + public void Add(T value) + { + if (List != null) + { + List.Add(value); + } + else + { + if (!_isSingleSet) + { + Single = value; + + _isSingleSet = true; + } + else + { + List = new List(); + + List.Add(Single); + List.Add(value); + + Single = default; + } + } + } + + /// + /// Removes a value. + /// + /// Value to remove. + public bool Remove(T value) + { + if (List != null) + { + return List.Remove(value); + } + + if (!_isSingleSet) + { + return false; + } + + if (EqualityComparer.Default.Equals(Single, value)) + { + Single = default; + + _isSingleSet = false; + + return true; + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index dec1f62386..58ebc48652 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Data; using Avalonia.PropertyStore; using Avalonia.Utilities; @@ -8,26 +7,37 @@ using Avalonia.Utilities; namespace Avalonia { + /// + /// Stores styled property values for an . + /// + /// + /// At its core this class consists of an to + /// mapping which holds the current values for each set property. This + /// can be in one of 4 states: + /// + /// - For a single local value it will be an instance of . + /// - For a single value of a priority other than LocalValue it will be an instance of + /// ` + /// - For a single binding it will be an instance of + /// - For all other cases it will be an instance of + /// internal class ValueStore : IValueSink { private readonly AvaloniaObject _owner; private readonly IValueSink _sink; - private readonly AvaloniaPropertyValueStore _values; + private readonly AvaloniaPropertyValueStore _values; public ValueStore(AvaloniaObject owner) { _sink = _owner = owner; - _values = new AvaloniaPropertyValueStore(); + _values = new AvaloniaPropertyValueStore(); } public bool IsAnimating(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) - { - return v.ValuePriority < BindingPriority.LocalValue; - } + return slot.ValuePriority < BindingPriority.LocalValue; } return false; @@ -37,10 +47,7 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) - { - return v.Value.HasValue; - } + return slot.Value.HasValue; } return false; @@ -50,13 +57,12 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue v) + var v = (IValue)slot; + + if (v.Value.HasValue) { - if (v.Value.HasValue) - { - value = v.Value.Value; - return true; - } + value = v.Value.Value; + return true; } } @@ -165,14 +171,11 @@ namespace Avalonia { if (_values.TryGetValue(property, out var slot)) { - if (slot is IValue value) - { - return new Diagnostics.AvaloniaPropertyValue( - property, - value.Value.HasValue ? (object)value.Value : AvaloniaProperty.UnsetValue, - value.ValuePriority, - null); - } + return new Diagnostics.AvaloniaPropertyValue( + property, + slot.Value.HasValue ? (object)slot.Value : AvaloniaProperty.UnsetValue, + slot.ValuePriority, + null); } return null; diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs index 86113da87e..bea6f01243 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs @@ -238,7 +238,7 @@ namespace Avalonia.Collections } else { - return seq.ThenByDescending(o => GetValue(o), InternalComparer); + return seq.ThenBy(o => GetValue(o), InternalComparer); } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs index 060922238d..9feca71cda 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// public class DataGridFrozenGrid : Grid { - public static readonly AvaloniaProperty IsFrozenProperty = + public static readonly StyledProperty IsFrozenProperty = AvaloniaProperty.RegisterAttached("IsFrozen"); /// diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs index f15442addf..489bfc31d0 100644 --- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; -[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 59c6c47ed9..c49f7c3d09 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -32,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode { /// /// The application-global data templates. @@ -45,6 +45,12 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + StyledElement.DataContextProperty.AddOwner(); + /// public event EventHandler ResourcesChanged; @@ -56,6 +62,19 @@ namespace Avalonia Name = "Avalonia Application"; } + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 1e0fa01cc3..0bc6394b19 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -326,7 +326,7 @@ namespace Avalonia.Controls if (property == IsPressedProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 6d4e2e3373..2ac9319478 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -215,7 +215,7 @@ namespace Avalonia.Controls if (property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 6f121afbef..38ebbe5bf9 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -358,7 +358,11 @@ namespace Avalonia.Controls /// private static bool SharedSizeGroupPropertyValueValid(string value) { - Contract.Requires(value != null); + // null is default value + if (value == null) + { + return true; + } string id = (string)value; diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index b30a8668fd..ee27aa7ec1 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -1,9 +1,11 @@ -using Avalonia.Controls.Shapes; +using System; +using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Metadata; namespace Avalonia.Controls { + [Obsolete("Use Image control with DrawingImage source")] public class DrawingPresenter : Control { static DrawingPresenter() diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index b16159fea0..e42d3ec1e5 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls if (property == ExpandDirectionProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs index e2e591215e..5c931bc771 100644 --- a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs @@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators /// Gets the container index for the tree. /// TreeContainerIndex Index { get; } + + /// + /// Updates the index based on the parent . + /// + void UpdateIndex(); } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index c06a64443c..22e06a40b1 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Generators { @@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { + private TreeView _treeView; + /// /// Initializes a new instance of the class. /// @@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators /// The container's ContentTemplate property. /// The container's Items property. /// The container's IsExpanded property. - /// The container index for the tree public TreeItemContainerGenerator( IControl owner, AvaloniaProperty contentProperty, AvaloniaProperty contentTemplateProperty, AvaloniaProperty itemsProperty, - AvaloniaProperty isExpandedProperty, - TreeContainerIndex index) + AvaloniaProperty isExpandedProperty) : base(owner, contentProperty, contentTemplateProperty) { Contract.Requires(owner != null); Contract.Requires(contentProperty != null); Contract.Requires(itemsProperty != null); Contract.Requires(isExpandedProperty != null); - Contract.Requires(index != null); ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; - Index = index; + UpdateIndex(); } /// /// Gets the container index for the tree. /// - public TreeContainerIndex Index { get; } + public TreeContainerIndex Index { get; private set; } /// /// Gets the item container's Items property. @@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators } else if (container != null) { - Index.Add(item, container); + Index?.Add(item, container); return container; } else @@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - Index.Add(item, result); + Index?.Add(item, result); return result; } @@ -101,24 +102,44 @@ namespace Avalonia.Controls.Generators public override IEnumerable Clear() { var items = base.Clear(); - Index.Remove(0, items); + Index?.Remove(0, items); return items; } public override IEnumerable Dematerialize(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } public override IEnumerable RemoveRange(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.RemoveRange(startingIndex, count); } public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + public void UpdateIndex() + { + if (Owner is TreeView treeViewOwner && Index == null) + { + Index = new TreeContainerIndex(); + _treeView = treeViewOwner; + } + else + { + var treeView = Owner.GetSelfAndLogicalAncestors().OfType().FirstOrDefault(); + + if (treeView != _treeView) + { + Clear(); + Index = treeView?.ItemContainerGenerator?.Index; + _treeView = treeView; + } + } + } + class WrapperTreeDataTemplate : ITreeDataTemplate { private readonly IDataTemplate _inner; diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index a2fefa0548..a2d53f4f06 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -23,37 +23,37 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeDirectionProperty = + public static readonly StyledProperty ResizeDirectionProperty = AvaloniaProperty.Register(nameof(ResizeDirection)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ResizeBehaviorProperty = + public static readonly StyledProperty ResizeBehaviorProperty = AvaloniaProperty.Register(nameof(ResizeBehavior)); /// /// Defines the property. /// - public static readonly AvaloniaProperty ShowsPreviewProperty = + public static readonly StyledProperty ShowsPreviewProperty = AvaloniaProperty.Register(nameof(ShowsPreview)); /// /// Defines the property. /// - public static readonly AvaloniaProperty KeyboardIncrementProperty = + public static readonly StyledProperty KeyboardIncrementProperty = AvaloniaProperty.Register(nameof(KeyboardIncrement), 10d); /// /// Defines the property. /// - public static readonly AvaloniaProperty DragIncrementProperty = + public static readonly StyledProperty DragIncrementProperty = AvaloniaProperty.Register(nameof(DragIncrement), 1d); /// /// Defines the property. /// - public static readonly AvaloniaProperty> PreviewContentProperty = + public static readonly StyledProperty> PreviewContentProperty = AvaloniaProperty.Register>(nameof(PreviewContent)); private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast); diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index ff6cd482df..41b6e5449a 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. @@ -23,6 +23,14 @@ namespace Avalonia.Controls public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchDirectionProperty = + AvaloniaProperty.Register( + nameof(StretchDirection), + StretchDirection.Both); + static Image() { AffectsRender(SourceProperty, StretchProperty); @@ -30,9 +38,9 @@ namespace Avalonia.Controls } /// - /// Gets or sets the bitmap image that will be displayed. + /// Gets or sets the image that will be displayed. /// - public IBitmap Source + public IImage Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -43,10 +51,19 @@ namespace Avalonia.Controls /// public Stretch Stretch { - get { return (Stretch)GetValue(StretchProperty); } + get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } + /// + /// Gets or sets a value controlling in what direction the image will be stretched. + /// + public StretchDirection StretchDirection + { + get { return GetValue(StretchDirectionProperty); } + set { SetValue(StretchDirectionProperty, value); } + } + /// /// Renders the control. /// @@ -58,8 +75,8 @@ namespace Avalonia.Controls if (source != null) { Rect viewPort = new Rect(Bounds.Size); - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize); + Size sourceSize = source.Size; + Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); Size scaledSize = sourceSize * scale; Rect destRect = viewPort .CenterRect(new Rect(scaledSize)) @@ -69,7 +86,7 @@ namespace Avalonia.Controls var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); - context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); + context.DrawImage(source, sourceRect, destRect, interpolationMode); } } @@ -85,15 +102,7 @@ namespace Avalonia.Controls if (source != null) { - Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); - if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) - { - result = sourceSize; - } - else - { - result = Stretch.CalculateSize(availableSize, sourceSize); - } + result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection); } return result; @@ -106,7 +115,7 @@ namespace Avalonia.Controls if (source != null) { - var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var sourceSize = source.Size; var result = Stretch.CalculateSize(finalSize, sourceSize); return result; } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bf22f0a08a..94c34b28d6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -324,20 +323,24 @@ namespace Avalonia.Controls return; } - var current = focus.Current - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.VisualParent == container); + IVisual current = focus.Current; - if (current != null) + while (current != null) { - var next = GetNextControl(container, direction.Value, current, false); - - if (next != null) + if (current.VisualParent == container && current is IInputElement inputElement) { - focus.Focus(next, NavigationMethod.Directional); - e.Handled = true; + IInputElement next = GetNextControl(container, direction.Value, inputElement, false); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + + break; } + + current = current.VisualParent; } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index db67a24159..a3bb654629 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -17,10 +17,10 @@ namespace Avalonia.Controls /// public class LayoutTransformControl : Decorator { - public static readonly AvaloniaProperty LayoutTransformProperty = + public static readonly StyledProperty LayoutTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); - public static readonly AvaloniaProperty UseRenderTransformProperty = + public static readonly StyledProperty UseRenderTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); static LayoutTransformControl() diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 4966e669ed..1761eeb3cf 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.InputModifiers & InputModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0); } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index ec3bd7d688..56044ea522 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Primitives; +using Avalonia.Rendering; using Avalonia.Data; using Avalonia.VisualTree; @@ -15,7 +16,7 @@ namespace Avalonia.Controls.Notifications /// /// An that displays notifications in a . /// - public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager + public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; @@ -150,7 +151,7 @@ namespace Avalonia.Controls.Notifications if (property == PositionProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } @@ -161,15 +162,9 @@ namespace Avalonia.Controls.Notifications /// The that will be the host. private void Install(Window host) { - var adornerLayer = host.GetVisualDescendants() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; - if (adornerLayer != null) - { - adornerLayer.Children.Add(this); - } + adornerLayer?.Children.Add(this); } private void UpdatePseudoClasses(NotificationPosition position) @@ -179,5 +174,7 @@ namespace Avalonia.Controls.Notifications PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft); PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight); } + + public bool HitTest(Point point) => VisualChildren.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 97aeead608..bf811ad008 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -6,6 +6,8 @@ using Avalonia.LogicalTree; using Avalonia.Rendering; using Avalonia.Threading; +#nullable enable + namespace Avalonia.Controls.Platform { /// @@ -14,8 +16,8 @@ namespace Avalonia.Controls.Platform public class DefaultMenuInteractionHandler : IMenuInteractionHandler { private readonly bool _isContextMenu; - private IDisposable _inputManagerSubscription; - private IRenderRoot _root; + private IDisposable? _inputManagerSubscription; + private IRenderRoot? _root; public DefaultMenuInteractionHandler(bool isContextMenu) : this(isContextMenu, Input.InputManager.Instance, DefaultDelayRun) @@ -24,9 +26,11 @@ namespace Avalonia.Controls.Platform public DefaultMenuInteractionHandler( bool isContextMenu, - IInputManager inputManager, + IInputManager? inputManager, Action delayRun) { + delayRun = delayRun ?? throw new ArgumentNullException(nameof(delayRun)); + _isContextMenu = isContextMenu; InputManager = inputManager; DelayRun = delayRun; @@ -92,7 +96,7 @@ namespace Avalonia.Controls.Platform root.Deactivated -= WindowDeactivated; } - _inputManagerSubscription.Dispose(); + _inputManagerSubscription!.Dispose(); Menu = null; _root = null; @@ -100,9 +104,9 @@ namespace Avalonia.Controls.Platform protected Action DelayRun { get; } - protected IInputManager InputManager { get; } + protected IInputManager? InputManager { get; } - protected IMenu Menu { get; private set; } + protected IMenu? Menu { get; private set; } protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); @@ -131,7 +135,7 @@ namespace Avalonia.Controls.Platform KeyDown(GetMenuItem(e.Source as IControl), e); } - protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) + protected internal virtual void KeyDown(IMenuItem? item, KeyEventArgs e) { switch (e.Key) { @@ -200,7 +204,7 @@ namespace Avalonia.Controls.Platform } else { - Menu.Close(); + Menu!.Close(); } e.Handled = true; @@ -213,12 +217,12 @@ namespace Avalonia.Controls.Platform { if (item == null && _isContextMenu) { - if (Menu.MoveSelection(direction.Value, true) == true) + if (Menu!.MoveSelection(direction.Value, true) == true) { e.Handled = true; } } - else if (item.Parent?.MoveSelection(direction.Value, true) == true) + else if (item?.Parent?.MoveSelection(direction.Value, true) == true) { // If the the parent is an IMenu which successfully moved its selection, // and the current menu is open then close the current menu and open the @@ -408,7 +412,7 @@ namespace Avalonia.Controls.Platform protected void CloseMenu(IMenuItem item) { - var current = (IMenuElement)item; + var current = (IMenuElement?)item; while (current != null && !(current is IMenu)) { @@ -456,7 +460,7 @@ namespace Avalonia.Controls.Platform protected void SelectItemAndAncestors(IMenuItem item) { - var current = item; + var current = (IMenuItem?)item; while (current?.Parent != null) { @@ -465,7 +469,7 @@ namespace Avalonia.Controls.Platform } } - protected static IMenuItem GetMenuItem(IControl item) + protected static IMenuItem? GetMenuItem(IControl? item) { while (true) { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 5e1a844720..9b1215c9ae 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property. /// - public static readonly AvaloniaProperty BorderBrushProperty = + public static readonly StyledProperty BorderBrushProperty = Border.BorderBrushProperty.AddOwner(); /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index cd14211075..d27de7a80d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -188,8 +188,8 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Remove: - if (e.OldStartingIndex >= FirstIndex && - e.OldStartingIndex < NextIndex) + if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) || + panel.Children.Count > ItemCount) { RecycleContainersOnRemove(); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e0cc9aa128..9084012619 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index ebe5e0a93e..9a2f0310d7 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -138,10 +138,7 @@ namespace Avalonia.Controls.Primitives } } - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + public bool HitTest(Point point) => Children.HitTestCustom(point); private class AdornedElementInfo { diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 487a5e91e4..5150033a53 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -21,11 +21,8 @@ namespace Avalonia.Controls.Primitives return null; } - - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + + public bool HitTest(Point point) => Children.HitTestCustom(point); protected override Size ArrangeOverride(Size finalSize) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 3010a3d8a8..f0358ec04f 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -306,7 +306,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (placement == PlacementMode.Pointer) { positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); - positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; positionerParameters.Gravity = PopupPositioningEdge.BottomRight; } else diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index f1ee7c0e1a..baa51f92ec 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Minimum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Maximum"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives set { - ValidateDouble(value, "Value"); + if (!ValidateDouble(value)) + { + return; + } if (IsInitialized) { @@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double value is NaN or Inf. + /// Checks if the double value is not inifinity nor NaN. /// /// The value. - /// The name of the property being set. - private static void ValidateDouble(double value, string property) + private static bool ValidateDouble(double value) { - if (double.IsInfinity(value) || double.IsNaN(value)) - { - throw new ArgumentException($"{value} is not a valid value for {property}."); - } + return !double.IsInfinity(value) || !double.IsNaN(value); } /// diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index d7ba899ff3..4d3ae3c5b8 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -151,7 +151,7 @@ namespace Avalonia.Controls.Primitives if (property == OrientationProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 329b086a7c..69da211aa4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -13,7 +14,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Logging; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -241,17 +241,14 @@ namespace Avalonia.Controls.Primitives public override void BeginInit() { base.BeginInit(); - ++_updateCount; - _updateSelectedIndex = int.MinValue; + + InternalBeginInit(); } /// public override void EndInit() { - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); base.EndInit(); } @@ -269,11 +266,20 @@ namespace Avalonia.Controls.Primitives /// The container or null if the event did not originate in a container. protected IControl GetContainerFromEventSource(IInteractive eventSource) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); + var parent = (IVisual)eventSource; - return item; + while (parent != null) + { + if (parent is IControl control && control.LogicalParent == this + && ItemContainerGenerator?.IndexFromContainer(control) != -1) + { + return control; + } + + parent = parent.VisualParent; + } + + return null; } /// @@ -429,7 +435,8 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_updateCount; + + InternalBeginInit(); } /// @@ -437,10 +444,7 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextEndUpdate(); - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); } protected override void OnKeyDown(KeyEventArgs e) @@ -1110,6 +1114,26 @@ namespace Avalonia.Controls.Primitives } } + private void InternalBeginInit() + { + if (_updateCount == 0) + { + _updateSelectedIndex = int.MinValue; + } + + ++_updateCount; + } + + private void InternalEndInit() + { + Debug.Assert(_updateCount > 0); + + if (--_updateCount == 0) + { + UpdateFinished(); + } + } + private class Selection : IEnumerable { private readonly List _list = new List(); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 1b3cb09c1f..c4f1f2c7e7 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -1,12 +1,20 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Data; +using Avalonia.Interactivity; namespace Avalonia.Controls.Primitives { + /// + /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. + /// public class ToggleButton : Button { + /// + /// Defines the property. + /// public static readonly DirectProperty IsCheckedProperty = AvaloniaProperty.RegisterDirect( nameof(IsChecked), @@ -15,19 +23,71 @@ namespace Avalonia.Controls.Primitives unsetValue: null, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// public static readonly StyledProperty IsThreeStateProperty = AvaloniaProperty.Register(nameof(IsThreeState)); + /// + /// Defines the event. + /// + public static readonly RoutedEvent CheckedEvent = + RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent UncheckedEvent = + RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IndeterminateEvent = + RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble); + private bool? _isChecked = false; public ToggleButton() { UpdatePseudoClasses(IsChecked); + IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); + } + + /// + /// Raised when a is checked. + /// + public event EventHandler Checked + { + add => AddHandler(CheckedEvent, value); + remove => RemoveHandler(CheckedEvent, value); + } + + /// + /// Raised when a is unchecked. + /// + public event EventHandler Unchecked + { + add => AddHandler(UncheckedEvent, value); + remove => RemoveHandler(UncheckedEvent, value); } + /// + /// Raised when a is neither checked nor unchecked. + /// + public event EventHandler Indeterminate + { + add => AddHandler(IndeterminateEvent, value); + remove => RemoveHandler(IndeterminateEvent, value); + } + + /// + /// Gets or sets whether the is checked. + /// public bool? IsChecked { - get { return _isChecked; } + get => _isChecked; set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); @@ -35,6 +95,9 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets or sets a value that indicates whether the control supports three states. + /// public bool IsThreeState { get => GetValue(IsThreeStateProperty); @@ -47,18 +110,78 @@ namespace Avalonia.Controls.Primitives base.OnClick(); } + /// + /// Toggles the property. + /// protected virtual void Toggle() { if (IsChecked.HasValue) + { if (IsChecked.Value) + { if (IsThreeState) + { IsChecked = null; + } else + { IsChecked = false; + } + } else + { IsChecked = true; + } + } else + { IsChecked = false; + } + } + + /// + /// Called when becomes true. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnChecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes false. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnUnchecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes null. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnIndeterminate(RoutedEventArgs e) + { + RaiseEvent(e); + } + + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool?)e.NewValue; + + switch (newValue) + { + case true: + OnChecked(new RoutedEventArgs(CheckedEvent)); + break; + case false: + OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + break; + default: + OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + break; + } } private void UpdatePseudoClasses(bool? isChecked) diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index e4de6ac00f..1e02d70fff 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -290,7 +290,7 @@ namespace Avalonia.Controls.Primitives if (property == OrientationProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index b7229eb121..385e7cc567 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Avalonia.LogicalTree; -using Avalonia.Styling; namespace Avalonia.Controls.Primitives { @@ -8,7 +7,7 @@ namespace Avalonia.Controls.Primitives { private const int AdornerZIndex = int.MaxValue - 100; private const int OverlayZIndex = int.MaxValue - 99; - private IStyleHost _styleRoot; + private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); @@ -53,7 +52,7 @@ namespace Avalonia.Controls.Primitives layer.ZIndex = zindex; VisualChildren.Add(layer); if (((ILogical)this).IsAttachedToLogicalTree) - ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot)); + ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this)); InvalidateArrange(); } @@ -61,7 +60,7 @@ namespace Avalonia.Controls.Primitives protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - _styleRoot = e.Root; + _logicalRoot = e.Root; foreach (var l in _layers) ((ILogical)l).NotifyAttachedToLogicalTree(e); @@ -69,7 +68,7 @@ namespace Avalonia.Controls.Primitives protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - _styleRoot = null; + _logicalRoot = null; base.OnDetachedFromLogicalTree(e); foreach (var l in _layers) ((ILogical)l).NotifyDetachedFromLogicalTree(e); diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index bfa31e3676..753c6e1b71 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -87,11 +87,11 @@ namespace Avalonia.Controls if (property == IsIndeterminateProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault(), null); + UpdatePseudoClasses(newValue.GetValueOrDefault(), null); } else if (property == OrientationProperty) { - UpdatePseudoClasses(null, newValue.ValueOrDefault()); + UpdatePseudoClasses(null, newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 539fe1ec4b..c7a1a24c25 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Remote Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride, new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen); } - context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), + context.DrawImage(_bitmap, new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height), new Rect(Bounds.Size)); } base.Render(context); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 457d72bd60..086599d0bb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + public static readonly StyledProperty HorizontalCacheLengthProperty = AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); /// @@ -40,16 +40,16 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AvaloniaProperty LayoutProperty = + public static readonly StyledProperty LayoutProperty = AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. /// - public static readonly AvaloniaProperty VerticalCacheLengthProperty = + public static readonly StyledProperty VerticalCacheLengthProperty = AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); - private static readonly AttachedProperty VirtualizationInfoProperty = + private static readonly StyledProperty VirtualizationInfoProperty = AvaloniaProperty.RegisterAttached("VirtualizationInfo"); internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); @@ -379,7 +379,7 @@ namespace Avalonia.Controls { if (property == ItemsProperty) { - var newEnumerable = newValue.ValueOrDefault(); + var newEnumerable = newValue.GetValueOrDefault(); var newDataSource = newEnumerable as ItemsSourceView; if (newEnumerable != null && newDataSource == null) { @@ -390,19 +390,19 @@ namespace Avalonia.Controls } else if (property == ItemTemplateProperty) { - OnItemTemplateChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); + OnItemTemplateChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == LayoutProperty) { - OnLayoutChanged(oldValue.ValueOrDefault(), newValue.ValueOrDefault()); + OnLayoutChanged(oldValue.GetValueOrDefault(), newValue.GetValueOrDefault()); } else if (property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = newValue.ValueOrDefault(); + _viewportManager.HorizontalCacheLength = newValue.GetValueOrDefault(); } else if (property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = newValue.ValueOrDefault(); + _viewportManager.VerticalCacheLength = newValue.GetValueOrDefault(); } base.OnPropertyChanged(property, oldValue, newValue, priority); @@ -559,33 +559,35 @@ namespace Avalonia.Controls if (Layout != null) { - if (Layout is VirtualizingLayout virtualLayout) - { - var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + try + { _processingItemsSourceChange = args; - try + if (Layout is VirtualizingLayout virtualLayout) { virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } - finally - { - _processingItemsSourceChange = null; - } - } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) - { - // Walk through all the elements and make sure they are cleared for - // non-virtualizing layouts. - foreach (var element in Children) + else if (Layout is NonVirtualizingLayout nonVirtualLayout) { - if (GetVirtualizationInfo(element).IsRealized) + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) { - ClearElementImpl(element); + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } } + + Children.Clear(); } } + finally + { + _processingItemsSourceChange = null; + } InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 51c14d47d6..7d005a30b4 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -109,11 +109,22 @@ namespace Avalonia.Controls public void ClearElementToElementFactory(IControl element) { - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - _owner.ItemTemplateShim.RecycleElement(_owner, element); + if (_owner.ItemTemplateShim != null) + { + _owner.ItemTemplateShim.RecycleElement(_owner, element); + } + else + { + // No ItemTemplate to recycle to, remove the element from the children collection. + if (!_owner.Children.Remove(element)) + { + throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection."); + } + } + + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToElementFactory(); if (_lastFocusedElement == element) @@ -121,9 +132,8 @@ namespace Avalonia.Controls // Focused element is going away. Remove the tracked last focused element // and pick a reasonable next focus if we can find one within the layout // realized elements. - MoveFocusFromClearedIndex(clearedIndex); + MoveFocusFromClearedIndex(virtInfo.Index); } - } private void MoveFocusFromClearedIndex(int clearedIndex) @@ -190,7 +200,8 @@ namespace Avalonia.Controls { if (virtInfo == null) { - throw new ArgumentException("Element is not a child of this ItemsRepeater."); + //Element is not a child of this ItemsRepeater. + return -1; } return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; @@ -515,21 +526,52 @@ namespace Avalonia.Controls return element; } + // There are several cases handled here with respect to which element gets returned and when DataContext is modified. + // + // 1. If there is no ItemTemplate: + // 1.1 If data is an IControl -> the data is returned + // 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data + // + // 2. If there is an ItemTemplate: + // 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data + // 2.2 If data is an IControl: + // 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is + // 2.2.2 If Element returned by the ElementFactory is not the same as the data + // -> Element that is fetched from the ElementFactory is returned and + // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself private IControl GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. + var data = _owner.ItemsSourceView.GetAt(index); + var providedElementFactory = _owner.ItemTemplateShim; + + ItemTemplateWrapper GetElementFactory() + { + if (providedElementFactory == null) + { + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + return _owner.ItemTemplateShim; + } - var itemTemplateFactory = _owner.ItemTemplateShim; - if (itemTemplateFactory == null) + return providedElementFactory; + } + + IControl GetElement() { - // If no ItemTemplate was provided, use a default - var factory = FuncDataTemplate.Default; - _owner.ItemTemplate = factory; - itemTemplateFactory = _owner.ItemTemplateShim; + if (providedElementFactory == null) + { + if (data is IControl dataAsElement) + { + return dataAsElement; + } + } + + var elementFactory = GetElementFactory(); + return elementFactory.GetElement(_owner, data); } - var data = _owner.ItemsSourceView.GetAt(index); - var element = itemTemplateFactory.GetElement(_owner, data); + var element = GetElement(); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) @@ -537,8 +579,11 @@ namespace Avalonia.Controls virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } - // Prepare the element - element.DataContext = data; + if (data != element) + { + // Prepare the element + element.DataContext = data; + } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 380dd0dd0e..e88a8bfb1b 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls if (property == OrientationProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault()); + UpdatePseudoClasses(newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 204540e431..76813af8e5 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -3,7 +3,6 @@ using System; using System.Reactive.Linq; -using System.Reflection; namespace Avalonia.Controls.Templates { @@ -102,7 +101,7 @@ namespace Avalonia.Controls.Templates /// private static bool IsInstance(object o, Type t) { - return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); + return t.IsInstanceOfType(o); } } } diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index 998ef0e9f2..eba503e815 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Reflection; using Avalonia.Data; namespace Avalonia.Controls.Templates @@ -75,7 +74,7 @@ namespace Avalonia.Controls.Templates /// private static bool IsInstance(object o, Type t) { - return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); + return t.IsInstanceOfType(o); } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c7855ddfd1..8b8c7285be 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,7 +352,7 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index becc4c8a61..b561a3423f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -390,8 +390,10 @@ namespace Avalonia.Controls { return; } + _undoRedoHelper.Snapshot(); HandleTextInput(text); + _undoRedoHelper.Snapshot(); } protected override void OnKeyDown(KeyEventArgs e) @@ -401,12 +403,12 @@ namespace Avalonia.Controls bool movement = false; bool selection = false; bool handled = false; - var modifiers = e.Modifiers; + var modifiers = e.KeyModifiers; var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - bool DetectSelection() => e.Modifiers.HasFlag(keymap.SelectionModifiers); + bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers); if (Match(keymap.SelectAll)) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..cabadac7d6 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; -using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,7 +13,6 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -33,7 +30,8 @@ namespace Avalonia.Controls ILayoutRoot, IRenderRoot, ICloseable, - IStyleRoot, + IStyleHost, + ILogicalRoot, IWeakSubscriber { /// @@ -269,6 +267,12 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); + ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); + + var visualArgs = new VisualTreeAttachmentEventArgs(this, this); + OnDetachedFromVisualTreeCore(visualArgs); + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); @@ -296,10 +300,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) - { - control.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); } /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 59844be8a6..6c8d58a8dd 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -393,8 +393,7 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); result.Index.Materialized += ContainerMaterialized; return result; } @@ -407,7 +406,7 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.InputModifiers & InputModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0); } } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 07d5497c14..4d24337c3a 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -98,17 +98,18 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); } /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); + _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); - + Level = CalculateDistanceFromLogicalParent(this) - 1; + ItemContainerGenerator.UpdateIndex(); if (ItemTemplate == null && _treeView?.ItemTemplate != null) { @@ -119,7 +120,7 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); - ItemContainerGenerator.Clear(); + ItemContainerGenerator.UpdateIndex(); } protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index d02202ca02..529aec1393 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -1,10 +1,8 @@ using System; -using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Reflection; namespace Avalonia.Controls.Utils { @@ -13,14 +11,14 @@ namespace Avalonia.Controls.Utils class FinderNode : IDisposable { private readonly IStyledElement _control; - private readonly TypeInfo _ancestorType; + private readonly Type _ancestorType; public IObservable Observable => _subject; private readonly Subject _subject = new Subject(); private FinderNode _child; private IDisposable _disposable; - public FinderNode(IStyledElement control, TypeInfo ancestorType) + public FinderNode(IStyledElement control, Type ancestorType) { _control = control; _ancestorType = ancestorType; @@ -33,7 +31,7 @@ namespace Avalonia.Controls.Utils private void OnValueChanged(IStyledElement next) { - if (next == null || _ancestorType.IsAssignableFrom(next.GetType().GetTypeInfo())) + if (next == null || _ancestorType.IsAssignableFrom(next.GetType())) _subject.OnNext(next); else { @@ -63,7 +61,7 @@ namespace Avalonia.Controls.Utils { return new AnonymousObservable(observer => { - var finder = new FinderNode(control, ancestorType.GetTypeInfo()); + var finder = new FinderNode(control, ancestorType); var subscription = finder.Observable.Subscribe(observer); finder.Init(); diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index a91e6a3a48..d6cf3df536 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -90,23 +90,17 @@ namespace Avalonia.Controls.Utils { var borderThickness = borders.Top; var top = borderThickness * 0.5; - var cornerRadius = (float)Math.Max(0, radii.TopLeft - borderThickness - top); - if (background != null) - { - var topLeft = new Point(borders.Left, borders.Top); - var bottomRight = new Point(size.Width - borders.Right, size.Height - borders.Bottom); - var innerRect = new Rect(topLeft, bottomRight); - context.FillRectangle(background, innerRect, cornerRadius); - } + IPen pen = null; - if (borderBrush != null && borderThickness > 0) + if (borderThickness > 0) { - var topLeft = new Point(top, top); - var bottomRight = new Point(size.Width - top, size.Height - top); - var outerRect = new Rect(topLeft, bottomRight); - context.DrawRectangle(new Pen(borderBrush, borderThickness), outerRect, (float)radii.TopLeft); + pen = new Pen(borderBrush, borderThickness); } + + var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness); + + context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1816a6c81d..f66a248aaf 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -336,7 +336,6 @@ namespace Avalonia.Controls if (close) { PlatformImpl?.Dispose(); - HandleClosed(); } } } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 8081528e55..f9737b461d 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -47,7 +47,8 @@ namespace Avalonia.FreeDesktop var fProcMounts = File.ReadAllLines(ProcMountsDir) .Select(x => x.Split(' ')) - .Select(x => (x[0], x[1])); + .Select(x => (x[0], x[1])) + .Where(x => !x.Item2.StartsWith("/snap/", StringComparison.InvariantCultureIgnoreCase)); var labelDirEnum = Directory.Exists(DevByLabelDir) ? new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index aa009770f6..00e68d629b 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -140,7 +140,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (e.Key == Key.LeftAlt) + if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt) { _altIsDown = true; @@ -218,6 +218,7 @@ namespace Avalonia.Input switch (e.Key) { case Key.LeftAlt: + case Key.RightAlt: _altIsDown = false; if (_ignoreAltUp) diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Input/DragDrop.cs index d39659cee3..723d577964 100644 --- a/src/Avalonia.Input/DragDrop.cs +++ b/src/Avalonia.Input/DragDrop.cs @@ -23,7 +23,7 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); - public static readonly AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); + public static readonly AttachedProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true); /// /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation. diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 25d3b6887f..bcd962bc31 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -19,7 +19,7 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) + private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) { if (target == null) return DragDropEffects.None; @@ -38,13 +38,13 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { var target = GetTarget(inputRoot, point); @@ -77,7 +77,7 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) + private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers) { try { @@ -100,16 +100,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; case RawDragEventType.DragLeave: DragLeave(e.Root); break; case RawDragEventType.Drop: - e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.Modifiers); + e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index dc0b76b225..08e9eb7d33 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,8 +13,11 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; private set; } + public KeyModifiers KeyModifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -32,13 +35,25 @@ namespace Avalonia.Input return point; } + [Obsolete("Use constructor taking KeyModifiers")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { - this.Data = data; - this._target = target; - this._targetLocation = targetLocation; - this.Modifiers = modifiers; + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = modifiers; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF); + } + + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + : base(routedEvent) + { + Data = data; + _target = target; + _targetLocation = targetLocation; + Modifiers = (InputModifiers)keyModifiers; + KeyModifiers = keyModifiers; } } diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 104ac9cb61..a9ce8ee494 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -17,8 +18,8 @@ namespace Avalonia.Input /// /// The focus scopes in which the focus is currently defined. /// - private readonly Dictionary _focusScopes = - new Dictionary(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -110,7 +111,18 @@ namespace Avalonia.Input { Contract.Requires(scope != null); - _focusScopes[scope] = element; + if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) + { + if (element != existingElement) + { + _focusScopes.Remove(scope); + _focusScopes.Add(scope, element); + } + } + else + { + _focusScopes.Add(scope, element); + } if (Scope == scope) { @@ -180,18 +192,18 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); + IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; - if (element == null || !CanFocus(element)) + while (element != null) { - element = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(CanFocus); - } + if (element is IInputElement inputElement && CanFocus(inputElement)) + { + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); - if (element != null) - { - Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + break; + } + + element = element.VisualParent; } } } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index a5bd4feb64..a656d08c16 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -79,7 +79,7 @@ namespace Avalonia.Input { s_lastPress = new WeakReference(e.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right) + else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton == MouseButton.Left) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { diff --git a/src/Avalonia.Input/GotFocusEventArgs.cs b/src/Avalonia.Input/GotFocusEventArgs.cs index 01e978a55c..8f9305e472 100644 --- a/src/Avalonia.Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Input/GotFocusEventArgs.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using Avalonia.Interactivity; namespace Avalonia.Input @@ -18,6 +19,16 @@ namespace Avalonia.Input /// /// Gets or sets any input modifiers active at the time of focus. /// - public InputModifiers InputModifiers { get; set; } + [Obsolete("Use KeyModifiers")] + public InputModifiers InputModifiers + { + get => (InputModifiers)KeyModifiers; + set => KeyModifiers = (KeyModifiers)((int)value & 0xF); + } + + /// + /// Gets or sets any key modifiers active at the time of focus. + /// + public KeyModifiers KeyModifiers { get; set; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 1a82f7d671..144979523f 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -48,6 +48,8 @@ namespace Avalonia.Input LeftMouseButton = 16, RightMouseButton = 32, MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 59dd6a7d4f..121412f2e4 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -345,7 +345,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the control is focused. + /// Gets a value indicating whether the control is focused. /// public bool IsFocused { @@ -363,7 +363,7 @@ namespace Avalonia.Input } /// - /// Gets or sets a value indicating whether the pointer is currently over the control. + /// Gets a value indicating whether the pointer is currently over the control. /// public bool IsPointerOver { @@ -535,11 +535,11 @@ namespace Avalonia.Input if (property == IsFocusedProperty) { - UpdatePseudoClasses(newValue.ValueOrDefault(), null); + UpdatePseudoClasses(newValue.GetValueOrDefault(), null); } else if (property == IsPointerOverProperty) { - UpdatePseudoClasses(null, newValue.ValueOrDefault()); + UpdatePseudoClasses(null, newValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f83c41e266..4dcee0bbc8 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -38,7 +38,9 @@ namespace Avalonia.Input /// The topmost at the specified position. public static IInputElement InputHitTest(this IInputElement element, Point p) { - return element.GetInputElementsAt(p).FirstOrDefault(); + Contract.Requires(element != null); + + return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement; } private static bool IsHitTestVisible(IVisual visual) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 4dcf0eee53..c74498171e 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -120,6 +120,10 @@ namespace Avalonia.Input rv++; if (props.IsRightButtonPressed) rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; return rv; } @@ -142,6 +146,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: if (ButtonCount(props) > 1) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -151,6 +157,8 @@ namespace Avalonia.Input case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: if (ButtonCount(props) != 0) e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); else @@ -186,12 +194,20 @@ namespace Avalonia.Input kind = PointerUpdateKind.MiddleButtonPressed; if (args.Type == RawPointerEventType.RightButtonDown) kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; if (args.Type == RawPointerEventType.MiddleButtonUp) kind = PointerUpdateKind.MiddleButtonReleased; if (args.Type == RawPointerEventType.RightButtonUp) kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; return new PointerPointProperties(args.InputModifiers, kind); } diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs index a758a328be..053f894755 100644 --- a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs +++ b/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs @@ -4,14 +4,14 @@ namespace Avalonia.Input.Platform { public class PlatformHotkeyConfiguration { - public PlatformHotkeyConfiguration() : this(InputModifiers.Control) + public PlatformHotkeyConfiguration() : this(KeyModifiers.Control) { } - public PlatformHotkeyConfiguration(InputModifiers commandModifiers, - InputModifiers selectionModifiers = InputModifiers.Shift, - InputModifiers wholeWordTextActionModifiers = InputModifiers.Control) + public PlatformHotkeyConfiguration(KeyModifiers commandModifiers, + KeyModifiers selectionModifiers = KeyModifiers.Shift, + KeyModifiers wholeWordTextActionModifiers = KeyModifiers.Control) { CommandModifiers = commandModifiers; SelectionModifiers = selectionModifiers; @@ -75,9 +75,9 @@ namespace Avalonia.Input.Platform }; } - public InputModifiers CommandModifiers { get; set; } - public InputModifiers WholeWordTextActionModifiers { get; set; } - public InputModifiers SelectionModifiers { get; set; } + public KeyModifiers CommandModifiers { get; set; } + public KeyModifiers WholeWordTextActionModifiers { get; set; } + public KeyModifiers SelectionModifiers { get; set; } public List Copy { get; set; } public List Cut { get; set; } public List Paste { get; set; } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 819d231b31..00222e92cf 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -55,9 +55,11 @@ namespace Avalonia.Input Captured.DetachedFromVisualTree += OnCaptureDetached; } - IInputElement GetNextCapture(IVisual parent) => - parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); - + IInputElement GetNextCapture(IVisual parent) + { + return parent as IInputElement ?? parent.FindAncestorOfType(); + } + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) { Capture(GetNextCapture(e.Parent)); diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 1068a0d4d4..96132fdc90 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -21,18 +21,24 @@ namespace Avalonia.Input public bool IsLeftButtonPressed { get; } public bool IsMiddleButtonPressed { get; } public bool IsRightButtonPressed { get; } + public bool IsXButton1Pressed { get; } + public bool IsXButton2Pressed { get; } + public PointerUpdateKind PointerUpdateKind { get; } + private PointerPointProperties() - { - + { } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; + IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton); IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton); IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton); + IsXButton1Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton1MouseButton); + IsXButton2Pressed = modifiers.HasFlagCustom(RawInputModifiers.XButton2MouseButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state @@ -49,6 +55,14 @@ namespace Avalonia.Input IsRightButtonPressed = true; if (kind == PointerUpdateKind.RightButtonReleased) IsRightButtonPressed = false; + if (kind == PointerUpdateKind.XButton1Pressed) + IsXButton1Pressed = true; + if (kind == PointerUpdateKind.XButton1Released) + IsXButton1Pressed = false; + if (kind == PointerUpdateKind.XButton2Pressed) + IsXButton2Pressed = true; + if (kind == PointerUpdateKind.XButton2Released) + IsXButton2Pressed = false; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -59,9 +73,13 @@ namespace Avalonia.Input LeftButtonPressed, MiddleButtonPressed, RightButtonPressed, + XButton1Pressed, + XButton2Pressed, LeftButtonReleased, MiddleButtonReleased, RightButtonReleased, + XButton1Released, + XButton2Released, Other } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 5722e17593..4193cdafc5 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input.Raw +using System; + +namespace Avalonia.Input.Raw { public class RawDragEvent : RawInputEventArgs { @@ -6,7 +8,9 @@ public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + [Obsolete("Use KeyModifiers")] public InputModifiers Modifiers { get; } + public KeyModifiers KeyModifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) @@ -17,6 +21,7 @@ Data = data; Effects = effects; Modifiers = (InputModifiers)modifiers; + KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); } } } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 56854c7d29..4b2c95943d 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -14,6 +14,10 @@ namespace Avalonia.Input.Raw RightButtonUp, MiddleButtonDown, MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, Move, Wheel, NonClientLeftButtonDown, diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index bc5dec9a90..55d9e61d87 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -3,7 +3,6 @@ using System; using System.Reactive.Subjects; -using System.Reflection; namespace Avalonia.Interactivity { @@ -29,7 +28,7 @@ namespace Avalonia.Interactivity Contract.Requires(name != null); Contract.Requires(eventArgsType != null); Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).GetTypeInfo().IsAssignableFrom(eventArgsType.GetTypeInfo())); + Contract.Requires(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)); EventArgsType = eventArgsType; Name = name; diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..7f44c80a64 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -72,6 +72,7 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, string layoutId) { @@ -94,14 +95,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); } RaiseLineArranged(); @@ -115,10 +116,11 @@ namespace Avalonia.Layout public Size Arrange( Size finalSize, VirtualizingLayoutContext context, + bool isWrapping, LineAlignment lineAlignment, string layoutId) { - ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( Math.Max(finalSize.Width, _lastExtent.Width), @@ -270,6 +272,7 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, string layoutId) { if (anchorIndex != -1) @@ -280,7 +283,7 @@ namespace Avalonia.Layout var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; @@ -301,7 +304,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +342,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); @@ -544,6 +547,7 @@ namespace Avalonia.Layout private void ArrangeVirtualizingLayout( Size finalSize, LineAlignment lineAlignment, + bool isWrapping, string layoutId) { // Walk through the realized elements one line at a time and @@ -563,7 +567,7 @@ namespace Avalonia.Layout if (_orientation.MajorStart(currentBounds) != currentLineOffset) { spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); spaceAtLineStart = _orientation.MinorStart(currentBounds); countInLine = 0; currentLineOffset = _orientation.MajorStart(currentBounds); @@ -580,7 +584,7 @@ namespace Avalonia.Layout if (countInLine > 0) { var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); } } } @@ -594,6 +598,8 @@ namespace Avalonia.Layout double spaceAtLineEnd, double lineSize, LineAlignment lineAlignment, + bool isWrapping, + Size finalSize, string layoutId) { for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) @@ -659,6 +665,14 @@ namespace Avalonia.Layout } bounds = bounds.Translate(-_lastExtent.Position); + + if (!isWrapping) + { + _orientation.SetMinorSize( + ref bounds, + Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize))); + } + var element = _elementManager.GetAt(rangeIndex); element.Arrange(bounds); } diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index cfb4b14b1c..c235bcd90f 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.VisualTree; namespace Avalonia.Layout { @@ -61,5 +62,31 @@ namespace Avalonia.Layout return availableSize; } + + /// + /// Invalidates measure for given control and all visual children recursively. + /// + public static void InvalidateSelfAndChildrenMeasure(ILayoutable control) + { + void InnerInvalidateMeasure(IVisual target) + { + if (target is ILayoutable targetLayoutable) + { + targetLayoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } + } + + InnerInvalidateMeasure(control); + } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 4732808b91..9d0a5c57ee 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using Avalonia.Logging; using Avalonia.VisualTree; @@ -694,12 +693,9 @@ namespace Avalonia.Layout } /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) - { - i.InvalidateMeasure(); - } + LayoutHelper.InvalidateSelfAndChildrenMeasure(this); base.OnVisualParentChanged(oldParent, newParent); } diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index fba91e66c7..5d27ba9199 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -20,25 +20,25 @@ namespace Avalonia.Layout /// public sealed override void InitializeForContext(LayoutContext context) { - InitializeForContextCore((VirtualizingLayoutContext)context); + InitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override void UninitializeForContext(LayoutContext context) { - UninitializeForContextCore((VirtualizingLayoutContext)context); + UninitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override Size Measure(LayoutContext context, Size availableSize) { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize); } /// public sealed override Size Arrange(LayoutContext context, Size finalSize) { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize); } /// @@ -49,7 +49,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +61,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +83,7 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -98,6 +98,6 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; } } diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs new file mode 100644 index 0000000000..d3dec83e9b --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -0,0 +1,14 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +namespace Avalonia.Layout +{ + /// + /// Represents the base class for layout context types that do not support virtualization. + /// + public abstract class NonVirtualizingLayoutContext : LayoutContext + { + } +} diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e95a87dd16..e8ad49e9b9 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -268,6 +268,7 @@ namespace Avalonia.Layout false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, LayoutId); @@ -279,6 +280,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + false, FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); @@ -298,7 +300,7 @@ namespace Avalonia.Layout { if (property == OrientationProperty) { - var orientation = newValue.ValueOrDefault(); + var orientation = newValue.GetValueOrDefault(); //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index feaf98553d..54c3ccbb90 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -111,6 +111,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -124,6 +130,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -220,6 +227,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -270,15 +286,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -300,7 +318,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -330,17 +350,21 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) { _orientation.SetMinorSize( ref extent, - !double.IsInfinity(availableSizeMinor) ? + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? availableSizeMinor : - Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); _orientation.SetMajorSize( ref extent, Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); @@ -399,7 +423,7 @@ namespace Avalonia.Layout // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -407,6 +431,7 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, LayoutId); @@ -422,6 +447,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + true, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, LayoutId); return new Size(value.Width, value.Height); @@ -441,7 +467,7 @@ namespace Avalonia.Layout { if (property == OrientationProperty) { - var orientation = newValue.ValueOrDefault(); + var orientation = newValue.GetValueOrDefault(); //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //i.e. the properties are the inverse of each other. @@ -450,29 +476,32 @@ namespace Avalonia.Layout } else if (property == MinColumnSpacingProperty) { - _minColumnSpacing = newValue.ValueOrDefault(); + _minColumnSpacing = newValue.GetValueOrDefault(); } else if (property == MinRowSpacingProperty) { - _minRowSpacing = newValue.ValueOrDefault(); + _minRowSpacing = newValue.GetValueOrDefault(); } else if (property == ItemsJustificationProperty) { - _itemsJustification = newValue.ValueOrDefault(); + _itemsJustification = newValue.GetValueOrDefault(); ; } else if (property == ItemsStretchProperty) { - _itemsStretch = newValue.ValueOrDefault(); - ; + _itemsStretch = newValue.GetValueOrDefault(); } else if (property == MinItemWidthProperty) { - _minItemWidth = newValue.ValueOrDefault(); + _minItemWidth = newValue.GetValueOrDefault(); } else if (property == MinItemHeightProperty) { - _minItemHeight = newValue.ValueOrDefault(); + _minItemHeight = newValue.GetValueOrDefault(); + } + else if (property == MaximumRowsOrColumnsProperty) + { + _maximumRowsOrColumns = newValue.GetValueOrDefault(); } InvalidateLayout(); @@ -502,7 +531,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) { diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 65c2a75b1c..1a2bdeef1e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -19,8 +19,9 @@ - - + + + diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 571475c7ea..756619fa9f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -101,7 +101,7 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index 67c383f6ae..1356dd58ff 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using SharpGen.Runtime; +using Avalonia.Platform; namespace Avalonia.Native { - public class CallbackBase : SharpGen.Runtime.IUnknown + public class CallbackBase : SharpGen.Runtime.IUnknown, IExceptionCallback { private uint _refCount; private bool _disposed; @@ -76,5 +78,15 @@ namespace Avalonia.Native { } + + public void RaiseException(Exception e) + { + if (AvaloniaLocator.Current.GetService() is PlatformThreadingInterface threadingInterface) + { + threadingInterface.TerminateNativeApp(); + + threadingInterface.DispatchException(ExceptionDispatchInfo.Capture(e)); + } + } } } diff --git a/src/Avalonia.Native/PlatformThreadingInterface.cs b/src/Avalonia.Native/PlatformThreadingInterface.cs index 353124a9d1..ba8cc83e83 100644 --- a/src/Avalonia.Native/PlatformThreadingInterface.cs +++ b/src/Avalonia.Native/PlatformThreadingInterface.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using System.Threading; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -43,6 +44,8 @@ namespace Avalonia.Native } readonly IAvnPlatformThreadingInterface _native; + private ExceptionDispatchInfo _exceptionDispatchInfo; + private CancellationTokenSource _exceptionCancellationSource; public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) { @@ -57,32 +60,49 @@ namespace Avalonia.Native public void RunLoop(CancellationToken cancellationToken) { - if (cancellationToken.CanBeCanceled == false) - _native.RunLoop(null); - else + _exceptionDispatchInfo?.Throw(); + var l = new object(); + _exceptionCancellationSource = new CancellationTokenSource(); + + var compositeCancellation = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; + + var cancellation = _native.CreateLoopCancellation(); + compositeCancellation.Register(() => { - var l = new object(); - var cancellation = _native.CreateLoopCancellation(); - cancellationToken.Register(() => + lock (l) { - lock (l) - { - cancellation?.Cancel(); - } - }); - try - { - _native.RunLoop(cancellation); + cancellation?.Cancel(); } - finally + }); + + try + { + _native.RunLoop(cancellation); + } + finally + { + lock (l) { - lock(l) - { - cancellation?.Dispose(); - cancellation = null; - } + cancellation?.Dispose(); + cancellation = null; } } + + if (_exceptionDispatchInfo != null) + { + _exceptionDispatchInfo.Throw(); + } + } + + public void DispatchException (ExceptionDispatchInfo exceptionInfo) + { + _exceptionDispatchInfo = exceptionInfo; + } + + public void TerminateNativeApp() + { + _exceptionCancellationSource?.Cancel(); } public void Signal(DispatcherPriority priority) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 8b397403ca..5d701dc8df 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -16,6 +16,34 @@ using Avalonia.Threading; namespace Avalonia.Native { + public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle + { + IAvnWindowBase _native; + + public MacOSTopLevelWindowHandle(IAvnWindowBase native) + { + _native = native; + } + + public IntPtr Handle => NSWindow; + + public string HandleDescriptor => "NSWindow"; + + public IntPtr NSView => _native.ObtainNSViewHandle(); + + public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + + public IntPtr GetNSViewRetained() + { + return _native.ObtainNSViewHandleRetained(); + } + + public IntPtr GetNSWindowRetained() + { + return _native.ObtainNSWindowHandleRetained(); + } + } + public abstract class WindowBaseImpl : IWindowBaseImpl, IFramebufferPlatformSurface { @@ -45,6 +73,9 @@ namespace Avalonia.Native protected void Init(IAvnWindowBase window, IAvnScreens screens) { _native = window; + + Handle = new MacOSTopLevelWindowHandle(window); + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; @@ -322,6 +353,11 @@ namespace Avalonia.Native public void SetCursor(IPlatformHandle cursor) { + if (_native == null) + { + return; + } + var newCursor = cursor as AvaloniaNativeCursor; newCursor = newCursor ?? (_cursorFactory.GetCursor(StandardCursorType.Arrow) as AvaloniaNativeCursor); _native.Cursor = newCursor.Cursor; @@ -349,6 +385,6 @@ namespace Avalonia.Native } - public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED"); + public IPlatformHandle Handle { get; private set; } } } diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index 5a4d625c41..f7437e5930 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using System.Reactive.Linq; -using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; using ReactiveUI; @@ -21,7 +19,7 @@ namespace Avalonia.ReactiveUI /// public int GetAffinityForView(Type view) { - return typeof(IVisual).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0; + return typeof(IVisual).IsAssignableFrom(view) ? 10 : 0; } /// diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 010acc3ae0..3a39beae94 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index f0f115afbc..10ae610345 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI /// ViewModel type. public class ReactiveWindow : Window, IViewFor where TViewModel : class { - public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel>(nameof(ViewModel)); /// @@ -41,4 +41,4 @@ namespace Avalonia.ReactiveUI set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index ac5db32c14..e111b15c75 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -58,7 +58,7 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty RouterProperty = + public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); /// @@ -118,4 +118,4 @@ namespace Avalonia.ReactiveUI Content = viewInstance; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 1bec5fc365..85768a39c3 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -16,14 +16,14 @@ namespace Avalonia.ReactiveUI /// /// for the property. /// - public static readonly AvaloniaProperty PageTransitionProperty = + public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. /// - public static readonly AvaloniaProperty DefaultContentProperty = + public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); /// @@ -72,4 +72,4 @@ namespace Avalonia.ReactiveUI await PageTransition.Start(null, this, true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Controls/ISetResourceParent.cs similarity index 61% rename from src/Avalonia.Styling/Styling/ISetStyleParent.cs rename to src/Avalonia.Styling/Controls/ISetResourceParent.cs index bca3d9d714..a1264adc34 100644 --- a/src/Avalonia.Styling/Styling/ISetStyleParent.cs +++ b/src/Avalonia.Styling/Controls/ISetResourceParent.cs @@ -1,29 +1,27 @@ -using Avalonia.Controls; - -namespace Avalonia.Styling +namespace Avalonia.Controls { /// - /// Defines an interface through which a 's parent can be set. + /// Defines an interface through which an 's parent can be set. /// /// /// You should not usually need to use this interface - it is for internal use only. /// - public interface ISetStyleParent : IStyle + public interface ISetResourceParent : IResourceNode { /// - /// Sets the style parent. + /// Sets the resource parent. /// /// The parent. void SetParent(IResourceNode parent); /// - /// Notifies the style that a change has been made to resources that apply to it. + /// Notifies the resource node that a change has been made to the resources in its parent. /// /// The event args. /// /// This method will be called automatically by the framework, you should not need to call /// this method yourself. /// - void NotifyResourcesChanged(ResourcesChangedEventArgs e); + void ParentResourcesChanged(ResourcesChangedEventArgs e); } } diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 901e27b7b7..acc2db1ff7 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -12,8 +12,12 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : AvaloniaDictionary, + IResourceDictionary, + IResourceNode, + ISetResourceParent { + private IResourceNode _parent; private AvaloniaList _mergedDictionaries; /// @@ -39,6 +43,12 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(this); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); @@ -48,11 +58,18 @@ namespace Avalonia.Controls }, x => { + if (x is ISetResourceParent setParent) + { + setParent.SetParent(null); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { OnResourcesChanged(); } + (x as ISetResourceParent)?.SetParent(null); x.ResourcesChanged -= MergedDictionaryResourcesChanged; }, () => { }); @@ -68,6 +85,27 @@ namespace Avalonia.Controls get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); } + /// + IResourceNode IResourceNode.ResourceParent => _parent; + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + NotifyMergedDictionariesResourcesChanged(e); + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceDictionary already has a parent."); + } + + _parent = parent; + } + /// public bool TryGetResource(object key, out object value) { @@ -95,7 +133,27 @@ namespace Avalonia.Controls ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); } - private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); + private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e) + { + if (_mergedDictionaries != null) + { + for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) + { + if (_mergedDictionaries[i] is ISetResourceParent merged) + { + merged.ParentResourcesChanged(e); + } + } + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var ev = new ResourcesChangedEventArgs(); + NotifyMergedDictionariesResourcesChanged(ev); + OnResourcesChanged(); + } + private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); } } diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Styling/IDataContextProvider.cs new file mode 100644 index 0000000000..31639c5784 --- /dev/null +++ b/src/Avalonia.Styling/IDataContextProvider.cs @@ -0,0 +1,13 @@ +namespace Avalonia +{ + /// + /// Defines an element with a data context that can be used for binding. + /// + public interface IDataContextProvider : IAvaloniaObject + { + /// + /// Gets or sets the element's data context. + /// + object DataContext { get; set; } + } +} diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index bcf1898c4c..d4d0f179c3 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -10,7 +10,8 @@ namespace Avalonia IStyleHost, ILogical, IResourceProvider, - IResourceNode + IResourceNode, + IDataContextProvider { /// /// Occurs when the control has finished initialization. @@ -27,11 +28,6 @@ namespace Avalonia /// new Classes Classes { get; set; } - /// - /// Gets or sets the control's data context. - /// - object DataContext { get; set; } - /// /// Gets the control's logical parent. /// diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 491f38c153..a8ec84bfa6 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -3,9 +3,6 @@ using System; using System.Linq; -using System.Reactive.Linq; -using System.Reflection; -using Avalonia.Controls; using Avalonia.Reactive; namespace Avalonia.LogicalTree @@ -25,7 +22,7 @@ namespace Avalonia.LogicalTree private readonly ILogical _relativeTo; private readonly int _ancestorLevel; private readonly Type _ancestorType; - ILogical _value; + private ILogical _value; public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { @@ -69,7 +66,7 @@ namespace Avalonia.LogicalTree private void Update() { _value = _relativeTo.GetLogicalAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .Where(x => _ancestorType?.IsAssignableFrom(x.GetType()) ?? true) .ElementAtOrDefault(_ancestorLevel); } } diff --git a/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs b/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs new file mode 100644 index 0000000000..4a61544a6f --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs @@ -0,0 +1,9 @@ +namespace Avalonia.LogicalTree +{ + /// + /// Represents a root of a logical tree. + /// + public interface ILogicalRoot : ILogical + { + } +} diff --git a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs index 1b0eb2b61b..20ea82202e 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Styling; namespace Avalonia.LogicalTree { @@ -16,16 +15,44 @@ namespace Avalonia.LogicalTree /// Initializes a new instance of the class. /// /// The root of the logical tree. - public LogicalTreeAttachmentEventArgs(IStyleHost root) + /// The control being attached/detached. + /// The . + public LogicalTreeAttachmentEventArgs( + ILogicalRoot root, + ILogical source, + ILogical parent) { Contract.Requires(root != null); + Contract.Requires(source != null); Root = root; + Source = source; + Parent = parent; } /// /// Gets the root of the logical tree that the control is being attached to or detached from. /// - public IStyleHost Root { get; } + public ILogicalRoot Root { get; } + + /// + /// Gets the control that was attached or detached from the logical tree. + /// + /// + /// Logical tree attachment events travel down the attached logical tree from the point of + /// attachment/detachment, so this control may be different from the control that the + /// event is being raised on. + /// + public ILogical Source { get; } + + /// + /// Gets the control that is being attached to or detached from. + /// + /// + /// For logical tree attachment, holds the new logical parent of . For + /// detachment, holds the old logical parent of . If the detachment event + /// was caused by a top-level control being closed, then this property will be null. + /// + public ILogical Parent { get; } } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 44cd1c4687..5768a25435 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -24,7 +24,7 @@ namespace Avalonia /// - Implements to form part of a logical tree. /// - A collection of class strings for custom styling. /// - public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent { /// /// Defines the property. @@ -59,7 +59,7 @@ namespace Avalonia private int _initCount; private string _name; private readonly Classes _classes = new Classes(); - private bool _isAttachedToLogicalTree; + private ILogicalRoot _logicalRoot; private IAvaloniaList _logicalChildren; private IResourceDictionary _resources; private Styles _styles; @@ -81,7 +81,7 @@ namespace Avalonia /// public StyledElement() { - _isAttachedToLogicalTree = this is IStyleRoot; + _logicalRoot = this as ILogicalRoot; } /// @@ -223,13 +223,13 @@ namespace Avalonia { if (_styles != null) { - (_styles as ISetStyleParent)?.SetParent(null); + (_styles as ISetResourceParent)?.SetParent(null); _styles.ResourcesChanged -= ThisResourcesChanged; } _styles = value; - if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + if (value is ISetResourceParent setParent && setParent.ResourceParent == null) { setParent.SetParent(this); } @@ -288,7 +288,7 @@ namespace Avalonia var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, - Validate = ValidateLogicalChild + Validate = logical => ValidateLogicalChild(logical) }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; @@ -307,7 +307,7 @@ namespace Avalonia /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// - bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; /// /// Gets the styled element's logical parent. @@ -367,7 +367,7 @@ namespace Avalonia throw new InvalidOperationException("BeginInit was not called."); } - if (--_initCount == 0 && _isAttachedToLogicalTree) + if (--_initCount == 0 && _logicalRoot != null) { InitializeStylesIfNeeded(); @@ -435,19 +435,6 @@ namespace Avalonia throw new InvalidOperationException("The Control already has a parent."); } - if (_isAttachedToLogicalTree) - { - var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot; - - if (oldRoot == null) - { - throw new AvaloniaInternalException("Was attached to logical tree but cannot find root."); - } - - var e = new LogicalTreeAttachmentEventArgs(oldRoot); - OnDetachedFromLogicalTreeCore(e); - } - if (InheritanceParent == null || parent == null) { InheritanceParent = parent as AvaloniaObject; @@ -455,6 +442,12 @@ namespace Avalonia Parent = (IStyledElement)parent; + if (_logicalRoot != null) + { + var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old); + OnDetachedFromLogicalTreeCore(e); + } + if (old != null) { old.ResourcesChanged -= ThisResourcesChanged; @@ -465,16 +458,16 @@ namespace Avalonia } ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); - if (Parent is IStyleRoot || Parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) + if (Parent is ILogicalRoot || Parent?.IsAttachedToLogicalTree == true || this is ILogicalRoot) { - var newRoot = FindStyleRoot(this); + var newRoot = FindLogicalRoot(this); if (newRoot == null) { throw new AvaloniaInternalException("Parent is attached to logical tree but cannot find root."); } - var e = new LogicalTreeAttachmentEventArgs(newRoot); + var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } @@ -601,11 +594,11 @@ namespace Avalonia } } - private static IStyleRoot FindStyleRoot(IStyleHost e) + private static ILogicalRoot FindLogicalRoot(IStyleHost e) { while (e != null) { - if (e is IStyleRoot root) + if (e is ILogicalRoot root) { return root; } @@ -631,15 +624,21 @@ namespace Avalonia private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { + if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) + { + throw new InvalidOperationException( + $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); + } + // This method can be called when a control is already attached to the logical tree // in the following scenario: // - ListBox gets assigned Items containing ListBoxItem // - ListBox makes ListBoxItem a logical child // - ListBox template gets applied; making its Panel get attached to logical tree // - That AttachedToLogicalTree signal travels down to the ListBoxItem - if (!_isAttachedToLogicalTree) + if (_logicalRoot == null) { - _isAttachedToLogicalTree = true; + _logicalRoot = e.Root; InitializeStylesIfNeeded(true); @@ -655,9 +654,9 @@ namespace Avalonia private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { - if (_isAttachedToLogicalTree) + if (_logicalRoot != null) { - _isAttachedToLogicalTree = false; + _logicalRoot = null; _styleDetach.OnNext(this); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); diff --git a/src/Avalonia.Styling/Styling/IGlobalStyles.cs b/src/Avalonia.Styling/Styling/IGlobalStyles.cs index 8bdf6f869b..51393ef0b3 100644 --- a/src/Avalonia.Styling/Styling/IGlobalStyles.cs +++ b/src/Avalonia.Styling/Styling/IGlobalStyles.cs @@ -6,7 +6,7 @@ namespace Avalonia.Styling /// /// Defines the style host that provides styles global to the application. /// - public interface IGlobalStyles : IStyleRoot + public interface IGlobalStyles : IStyleHost { } } diff --git a/src/Avalonia.Styling/Styling/IStyleRoot.cs b/src/Avalonia.Styling/Styling/IStyleRoot.cs deleted file mode 100644 index 56778dcb9f..0000000000 --- a/src/Avalonia.Styling/Styling/IStyleRoot.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Styling -{ - /// - /// Denotes the root in a tree. - /// - public interface IStyleRoot : IStyleHost - { - } -} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index af209ea970..7d4e92baeb 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Utilities; namespace Avalonia.Styling { @@ -41,7 +43,8 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - var inputs = new List>(); + ValueSingleOrList> inputs = default; + var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -66,19 +69,25 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { + Debug.Assert(match.Activator != null); + inputs.Add(match.Activator); } selector = selector.MovePrevious(); } - if (inputs.Count > 0) + if (inputs.HasList) + { + return new SelectorMatch(StyleActivator.And(inputs.List)); + } + else if (inputs.IsSingle) { - return new SelectorMatch(StyleActivator.And(inputs)); + return new SelectorMatch(inputs.Single); } else { - return alwaysThisType && !hitCombinator ? + return alwaysThisType && !hitCombinator ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 9312d38c51..9244232ff5 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -3,9 +3,7 @@ using System; using System.Reactive.Disposables; -using System.Reflection; using Avalonia.Animation; -using Avalonia.Controls; using Avalonia.Data; using Avalonia.Metadata; using Avalonia.Reactive; @@ -92,14 +90,15 @@ namespace Avalonia.Styling if (binding == null) { - var template = value as ITemplate; - bool isPropertyOfTypeITemplate = typeof(ITemplate).GetTypeInfo() - .IsAssignableFrom(Property.PropertyType.GetTypeInfo()); - - if (template != null && !isPropertyOfTypeITemplate) + if (value is ITemplate template) { - var materialized = template.Build(); - value = materialized; + bool isPropertyOfTypeITemplate = typeof(ITemplate).IsAssignableFrom(Property.PropertyType); + + if (!isPropertyOfTypeITemplate) + { + var materialized = template.Build(); + value = materialized; + } } if (activator == null) diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3ce82b4160..99ee8d8563 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, ISetStyleParent + public class Style : AvaloniaObject, IStyle, ISetResourceParent { private static Dictionary _applied = new Dictionary(); @@ -59,16 +59,16 @@ namespace Avalonia.Styling if (_resources != null) { - hadResources = _resources.Count > 0; + hadResources = _resources.HasResources; _resources.ResourcesChanged -= ResourceDictionaryChanged; } _resources = value; _resources.ResourcesChanged += ResourceDictionaryChanged; - if (hadResources || _resources.Count > 0) + if (hadResources || _resources.HasResources) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -194,13 +194,13 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index a4563110a9..0226288998 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { private IResourceNode _parent; private IResourceDictionary _resources; @@ -27,10 +27,10 @@ namespace Avalonia.Styling _styles.ForEachItem( x => { - if (x.ResourceParent == null && x is ISetStyleParent setParent) + if (x.ResourceParent == null && x is ISetResourceParent setParent) { setParent.SetParent(this); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -43,10 +43,10 @@ namespace Avalonia.Styling }, x => { - if (x.ResourceParent == this && x is ISetStyleParent setParent) + if (x.ResourceParent == this && x is ISetResourceParent setParent) { setParent.SetParent(null); - setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); } if (x.HasResources) @@ -98,7 +98,7 @@ namespace Avalonia.Styling if (hadResources || _resources.Count > 0) { - ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -246,7 +246,7 @@ namespace Avalonia.Styling IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { @@ -257,7 +257,7 @@ namespace Avalonia.Styling } /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); } @@ -266,7 +266,7 @@ namespace Avalonia.Styling { foreach (var child in this) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } ResourcesChanged?.Invoke(this, e); @@ -280,7 +280,7 @@ namespace Avalonia.Styling { if (foundSource) { - (child as ISetStyleParent)?.NotifyResourcesChanged(e); + (child as ISetResourceParent)?.ParentResourcesChanged(e); } foundSource |= child == sender; diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index f1fd2f6c7f..401fa54fb5 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -114,7 +114,7 @@ namespace Avalonia.Styling } else { - if (!TargetType.GetTypeInfo().IsAssignableFrom(controlType.GetTypeInfo())) + if (!TargetType.IsAssignableFrom(controlType)) { return SelectorMatch.NeverThisType; } diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index f3def542fc..8bb0fc297c 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -1,10 +1,15 @@ diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index e7cbed8ec5..4cdb116cdd 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -13,7 +13,6 @@ Height="18" VerticalAlignment="Center"/> + - \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 92b7dae904..d05dbac574 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -306,7 +306,7 @@ namespace Avalonia /// /// Parses a string. /// - /// The string. + /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new /// The . public static Matrix Parse(string s) { diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs new file mode 100644 index 0000000000..978a5b0c4c --- /dev/null +++ b/src/Avalonia.Visuals/Media/CharacterHit.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Media +{ + /// + /// Represents information about a character hit within a glyph run. + /// + /// + /// The CharacterHit structure provides information about the index of the first + /// character that got hit as well as information about leading or trailing edge. + /// + public readonly struct CharacterHit : IEquatable + { + /// + /// Initializes a new instance of the structure. + /// + /// Index of the first character that got hit. + /// In the case of a leading edge, this value is 0. In the case of a trailing edge, + /// this value is the number of code points until the next valid caret position. + public CharacterHit(int firstCharacterIndex, int trailingLength = 0) + { + FirstCharacterIndex = firstCharacterIndex; + + TrailingLength = trailingLength; + } + + /// + /// Gets the index of the first character that got hit. + /// + public int FirstCharacterIndex { get; } + + /// + /// Gets the trailing length value for the character that got hit. + /// + public int TrailingLength { get; } + + public bool Equals(CharacterHit other) + { + return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength; + } + + public override bool Equals(object obj) + { + return obj is CharacterHit other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return FirstCharacterIndex * 397 ^ TrailingLength; + } + } + + public static bool operator ==(CharacterHit left, CharacterHit right) + { + return left.Equals(right); + } + + public static bool operator !=(CharacterHit left, CharacterHit right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index a37463a0f0..3eb4f575c7 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using Avalonia.Animation; using Avalonia.Animation.Animators; namespace Avalonia.Media @@ -11,7 +10,7 @@ namespace Avalonia.Media /// /// An ARGB color. /// - public readonly struct Color + public readonly struct Color : IEquatable { static Color() { @@ -19,22 +18,22 @@ namespace Avalonia.Media } /// - /// Gets or sets the Alpha component of the color. + /// Gets the Alpha component of the color. /// public byte A { get; } /// - /// Gets or sets the Red component of the color. + /// Gets the Red component of the color. /// public byte R { get; } /// - /// Gets or sets the Green component of the color. + /// Gets the Green component of the color. /// public byte G { get; } /// - /// Gets or sets the Blue component of the color. + /// Gets the Blue component of the color. /// public byte B { get; } @@ -144,5 +143,40 @@ namespace Avalonia.Media { return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + + /// + /// Check if two colors are equal. + /// + public bool Equals(Color other) + { + return A == other.A && R == other.R && G == other.G && B == other.B; + } + + public override bool Equals(object obj) + { + return obj is Color other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(Color left, Color right) + { + return left.Equals(right); + } + + public static bool operator !=(Color left, Color right) + { + return !left.Equals(right); + } } } diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index 7784c73736..1e813edc13 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -14,13 +14,13 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty> DashesProperty = + public static readonly StyledProperty> DashesProperty = AvaloniaProperty.Register>(nameof(Dashes)); /// /// Defines the property. /// - public static readonly AvaloniaProperty OffsetProperty = + public static readonly StyledProperty OffsetProperty = AvaloniaProperty.Register(nameof(Offset)); private static ImmutableDashStyle s_dash; diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs index a60c591edc..6bc808e407 100644 --- a/src/Avalonia.Visuals/Media/Drawing.cs +++ b/src/Avalonia.Visuals/Media/Drawing.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media +using Avalonia.Platform; + +namespace Avalonia.Media { public abstract class Drawing : AvaloniaObject { @@ -6,4 +8,4 @@ public abstract Rect GetBounds(); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4c9bf9ebd4..4045b92c0c 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -74,18 +74,29 @@ namespace Avalonia.Media public Matrix CurrentContainerTransform => _currentContainerTransform; /// - /// Draws a bitmap image. + /// Draws an image. /// - /// The bitmap image. - /// The opacity to draw with. + /// The image. + /// The rect in the output to draw to. + public void DrawImage(IImage source, Rect rect) + { + Contract.Requires(source != null); + + DrawImage(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); - PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); + source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } /// @@ -118,6 +129,42 @@ namespace Avalonia.Media } } + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (Math.Abs(radiusX) > double.Epsilon) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (Math.Abs(radiusY) > double.Epsilon) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY); + } + /// /// Draws the outline of a rectangle. /// @@ -126,10 +173,7 @@ namespace Avalonia.Media /// The corner radius. public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) { - if (PenIsVisible(pen)) - { - PlatformImpl.DrawRectangle(pen, rect, cornerRadius); - } + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); } /// @@ -154,6 +198,22 @@ namespace Avalonia.Media } } + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + /// The baseline origin of the glyph run. + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + Contract.Requires(glyphRun != null); + + if (foreground != null) + { + PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + } + } + /// /// Draws a filled rectangle. /// @@ -162,10 +222,7 @@ namespace Avalonia.Media /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) { - if (brush != null && rect != Rect.Empty) - { - PlatformImpl.FillRectangle(brush, rect, cornerRadius); - } + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); } public readonly struct PushedState : IDisposable diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index 744ff2af03..e581c8c553 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -1,5 +1,6 @@ using Avalonia.Collections; using Avalonia.Metadata; +using Avalonia.Platform; namespace Avalonia.Media { @@ -55,4 +56,4 @@ namespace Avalonia.Media return rect; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs new file mode 100644 index 0000000000..57939bab24 --- /dev/null +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -0,0 +1,86 @@ +using System; +using Avalonia.Data; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// An that uses a for content. + /// + public class DrawingImage : AvaloniaObject, IImage, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or sets the drawing content. + /// + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + /// + public Size Size => Drawing?.GetBounds().Size ?? default; + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var drawing = Drawing; + + if (drawing == null) + { + return; + } + + var bounds = drawing.GetBounds(); + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y); + + using (context.PushClip(destRect)) + using (context.PushPreTransform(translate * scale)) + { + Drawing?.Draw(context); + } + } + + /// + protected override void OnPropertyChanged( + AvaloniaProperty property, + Optional oldValue, + BindingValue newValue, + BindingPriority priority) + { + base.OnPropertyChanged(property, oldValue, newValue, priority); + + if (property == DrawingProperty) + { + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + } +} diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index b57b4a0ca8..a69a93e416 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -2,17 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.Media.Fonts; namespace Avalonia.Media { public sealed class FontFamily { + public const string DefaultFontFamilyName = "$Default"; + static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(DefaultFontFamilyName); } /// @@ -57,15 +57,6 @@ namespace Avalonia.Media /// public static FontFamily Default { get; } - /// - /// Represents all font families in the system. This can be an expensive call depending on platform implementation. - /// - /// - /// Consider using the new instead. - /// - public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); - /// /// Gets the primary family name of the font family. /// @@ -86,10 +77,16 @@ namespace Avalonia.Media /// Gets the key for associated assets. /// /// - /// The family familyNames. + /// The family key. /// + /// Key is only used for custom fonts. public FontFamilyKey Key { get; } + /// + /// Returns True if this instance is the system's default. + /// + public bool IsDefault => Name.Equals(DefaultFontFamilyName); + /// /// Implicit conversion of string to FontFamily /// @@ -184,36 +181,40 @@ namespace Avalonia.Media { unchecked { - var hash = (int)2186146271; - - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } - else - { - hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); - } + return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + } + } - if (Key != null) - { - hash = (hash * 15768619) ^ Key.GetHashCode(); - } + public static bool operator !=(FontFamily a, FontFamily b) + { + return !(a == b); + } - return hash; + public static bool operator ==(FontFamily a, FontFamily b) + { + if (ReferenceEquals(a, b)) + { + return true; } + + return !(a is null) && a.Equals(b); } public override bool Equals(object obj) { + if (ReferenceEquals(this, obj)) + { + return true; + } + if (!(obj is FontFamily other)) { return false; } - if (Key != null) + if (!Equals(Key, other.Key)) { - return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key); + return false; } return other.FamilyNames.Equals(FamilyNames); diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index be1bd269ed..0c5e88b47a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.Media @@ -11,9 +13,53 @@ namespace Avalonia.Media /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts. /// It is also responsible for the font fallback. /// - public abstract class FontManager + public sealed class FontManager { - public static readonly FontManager Default = CreateDefault(); + private readonly ConcurrentDictionary _typefaceCache = + new ConcurrentDictionary(); + private readonly FontFamily _defaultFontFamily; + + private FontManager(IFontManagerImpl platformImpl) + { + PlatformImpl = platformImpl; + + DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + } + + public static FontManager Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + var renderInterface = AvaloniaLocator.Current.GetService(); + + var fontManagerImpl = renderInterface?.CreateFontManager(); + + if (fontManagerImpl == null) + { + return null; + } + + current = new FontManager(fontManagerImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// + /// + public IFontManagerImpl PlatformImpl { get; } /// /// Gets the system's default font family's name. @@ -21,25 +67,55 @@ namespace Avalonia.Media public string DefaultFontFamilyName { get; - protected set; } /// - /// Get all installed fonts in the system. - /// If true the font collection is updated. + /// Get all installed font family names. /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// If true the font collection is updated. + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Get a cached typeface from specified parameters. + /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. /// The font weight. /// The font style. /// - /// The cached typeface. + /// The typeface. /// - public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + { + while (true) + { + if (fontFamily.IsDefault) + { + fontFamily = _defaultFontFamily; + } + + var key = new FontKey(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryGetValue(key, out var typeface)) + { + return typeface; + } + + typeface = new Typeface(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryAdd(key, typeface)) + { + return typeface; + } + + if (fontFamily == _defaultFontFamily) + { + return null; + } + + fontFamily = _defaultFontFamily; + } + } /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -53,60 +129,13 @@ namespace Avalonia.Media /// /// The matched typeface. /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); - - public static FontManager CreateDefault() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if (platformImpl != null) - { - return new PlatformFontManager(platformImpl); - } - - return new EmptyFontManager(); - } - - private class PlatformFontManager : FontManager + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + FontStyle fontStyle = FontStyle.Normal, + FontFamily fontFamily = null, CultureInfo culture = null) { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManager(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - } - - private class EmptyFontManager : FontManager - { - public EmptyFontManager() - { - DefaultFontFamilyName = "Empty"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - new[] { DefaultFontFamilyName }; - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => null; + return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + null; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index e777d93315..cd08bba7b2 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { @@ -54,25 +55,19 @@ namespace Avalonia.Media.Fonts /// internal IReadOnlyList Names { get; } - /// /// - /// Returns an enumerator that iterates through the collection. + /// Returns an enumerator for the name collection. /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + public ImmutableReadOnlyListStructEnumerator GetEnumerator() { - return Names.GetEnumerator(); + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -111,7 +106,39 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - return ToString().GetHashCode(); + if (Count == 0) + { + return 0; + } + + unchecked + { + int hash = 17; + + for (var i = 0; i < Names.Count; i++) + { + string name = Names[i]; + + hash = hash * 23 + name.GetHashCode(); + } + + return hash; + } + } + + public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b) + { + return !(a == b); + } + + public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); } /// @@ -128,7 +155,20 @@ namespace Avalonia.Media.Fonts return false; } - return other.ToString().Equals(ToString()); + if (other.Count != Count) + { + return false; + } + + for (int i = 0; i < Count; i++) + { + if (Names[i] != other.Names[i]) + { + return false; + } + } + + return true; } public int Count => Names.Count; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 7733dd7d2a..887862face 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts } } + public static bool operator !=(FontFamilyKey a, FontFamilyKey b) + { + return !(a == b); + } + + public static bool operator ==(FontFamilyKey a, FontFamilyKey b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index 063fe8f20d..bed1fc6b83 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts { public static class FontFamilyLoader { - private static readonly IAssetLoader s_assetLoader; - - static FontFamilyLoader() - { - s_assetLoader = AvaloniaLocator.Current.GetService(); - } - /// /// Loads all font assets that belong to the specified /// @@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var assetLoader = AvaloniaLocator.Current.GetService(); + + var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); var matchingAssets = availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); @@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var assetLoader = AvaloniaLocator.Current.GetService(); + var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); - var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); string compareTo; diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs similarity index 65% rename from src/Skia/Avalonia.Skia/FontKey.cs rename to src/Avalonia.Visuals/Media/Fonts/FontKey.cs index bb3fe230c1..0ead585612 100644 --- a/src/Skia/Avalonia.Skia/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -2,24 +2,26 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Media; -namespace Avalonia.Skia +namespace Avalonia.Media.Fonts { - internal readonly struct FontKey : IEquatable + public readonly struct FontKey : IEquatable { + public readonly FontFamily FontFamily; public readonly FontStyle Style; public readonly FontWeight Weight; - public FontKey(FontWeight weight, FontStyle style) + public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) { + FontFamily = fontFamily; Style = style; Weight = weight; } public override int GetHashCode() { - var hash = 17; + var hash = FontFamily.GetHashCode(); + hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -33,7 +35,8 @@ namespace Avalonia.Skia public bool Equals(FontKey other) { - return Style == other.Style && + return FontFamily == other.FontFamily && + Style == other.Style && Weight == other.Weight; } } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index 3dad10fb8f..4df3aa8ae2 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -1,10 +1,13 @@ -namespace Avalonia.Media +using Avalonia.Metadata; + +namespace Avalonia.Media { public class GeometryDrawing : Drawing { public static readonly StyledProperty GeometryProperty = AvaloniaProperty.Register(nameof(Geometry)); + [Content] public Geometry Geometry { get => GetValue(GeometryProperty); diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs new file mode 100644 index 0000000000..43151deece --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -0,0 +1,457 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Utility; + +namespace Avalonia.Media +{ + /// + /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style. + /// + public sealed class GlyphRun : IDisposable + { + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + private IGlyphRunImpl _glyphRunImpl; + private GlyphTypeface _glyphTypeface; + private double _fontRenderingEmSize; + private Rect? _bounds; + + private ReadOnlySlice _glyphIndices; + private ReadOnlySlice _glyphAdvances; + private ReadOnlySlice _glyphOffsets; + private ReadOnlySlice _glyphClusters; + private ReadOnlySlice _characters; + + /// + /// Initializes a new instance of the class. + /// + public GlyphRun() + { + + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The glyph indices. + /// The glyph advances. + /// The glyph offsets. + /// The characters. + /// The glyph clusters. + /// The bidi level. + /// The bound. + public GlyphRun( + GlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlySlice glyphIndices, + ReadOnlySlice glyphAdvances = default, + ReadOnlySlice glyphOffsets = default, + ReadOnlySlice characters = default, + ReadOnlySlice glyphClusters = default, + int bidiLevel = 0, + Rect? bounds = null) + { + GlyphTypeface = glyphTypeface; + + FontRenderingEmSize = fontRenderingEmSize; + + GlyphIndices = glyphIndices; + + GlyphAdvances = glyphAdvances; + + GlyphOffsets = glyphOffsets; + + Characters = characters; + + GlyphClusters = glyphClusters; + + BidiLevel = bidiLevel; + + Initialize(bounds); + } + + /// + /// Gets or sets the for the . + /// + public GlyphTypeface GlyphTypeface + { + get => _glyphTypeface; + set => Set(ref _glyphTypeface, value); + } + + /// + /// Gets or sets the em size used for rendering the . + /// + public double FontRenderingEmSize + { + get => _fontRenderingEmSize; + set => Set(ref _fontRenderingEmSize, value); + } + + /// + /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. + /// + public ReadOnlySlice GlyphIndices + { + get => _glyphIndices; + set => Set(ref _glyphIndices, value); + } + + /// + /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. + /// + public ReadOnlySlice GlyphAdvances + { + get => _glyphAdvances; + set => Set(ref _glyphAdvances, value); + } + + /// + /// Gets or sets an array of values representing the offsets of the glyphs in the . + /// + public ReadOnlySlice GlyphOffsets + { + get => _glyphOffsets; + set => Set(ref _glyphOffsets, value); + } + + /// + /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . + /// + public ReadOnlySlice Characters + { + get => _characters; + set => Set(ref _characters, value); + } + + /// + /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// + public ReadOnlySlice GlyphClusters + { + get => _glyphClusters; + set => Set(ref _glyphClusters, value); + } + + /// + /// Gets or sets the bidirectional nesting level of the . + /// + public int BidiLevel + { + get; + set; + } + + /// + /// + /// + internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + /// + /// + /// + internal bool IsLeftToRight => ((BidiLevel & 1) == 0); + + /// + /// Gets or sets the conservative bounding box of the . + /// + public Rect Bounds + { + get + { + if (_bounds == null) + { + _bounds = CalculateBounds(); + } + + return _bounds.Value; + } + set => _bounds = value; + } + + public IGlyphRunImpl GlyphRunImpl + { + get + { + if (_glyphRunImpl == null) + { + Initialize(null); + } + + return _glyphRunImpl; + } + } + + public double GetDistanceFromCharacterHit(CharacterHit characterHit) + { + var distance = 0.0; + + var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + for (var i = 0; i < _glyphClusters.Length; i++) + { + if (_glyphClusters[i] >= end) + { + break; + } + + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[i]; + + distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + distance += GlyphAdvances[i]; + } + } + + return distance; + } + + public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) + { + // Before + if (distance < 0) + { + isInside = false; + + var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _); + + return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; + } + + //After + if (distance > Bounds.Size.Width) + { + isInside = false; + + var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _); + + return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); + } + + //Within + var currentX = 0.0; + var index = 0; + + for (; index < GlyphIndices.Length; index++) + { + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[index]; + + currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + currentX += GlyphAdvances[index]; + } + + if (currentX > distance) + { + break; + } + } + + if (index == GlyphIndices.Length) + { + index--; + } + + var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); + + isInside = distance < currentX && width > 0; + + var isTrailing = distance > currentX - width / 2; + + return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex); + } + + public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) + { + if (characterHit.TrailingLength == 0) + { + return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); + } + + var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + return new CharacterHit(nextCharacterHit.FirstCharacterIndex); + } + + public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + return characterHit.TrailingLength == 0 ? + FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) : + new CharacterHit(characterHit.FirstCharacterIndex); + } + + private class ReverseComparer : IComparer + { + public int Compare(T x, T y) + { + return Comparer.Default.Compare(y, x); + } + } + + private static readonly IComparer s_ascendingComparer = Comparer.Default; + private static readonly IComparer s_descendingComparer = new ReverseComparer(); + + internal CharacterHit FindNearestCharacterHit(int index, out double width) + { + width = 0.0; + + if (index < 0) + { + return default; + } + + var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; + + var clusters = _glyphClusters.AsSpan(); + + int start; + + if (index == 0 && clusters[0] == 0) + { + start = 0; + } + else + { + // Find the start of the cluster at the character index. + start = clusters.BinarySearch((ushort)index, comparer); + } + + // No cluster found. + if (start < 0) + { + while (index > 0 && start < 0) + { + index--; + + start = clusters.BinarySearch((ushort)index, comparer); + } + + if (start < 0) + { + return default; + } + } + + var trailingLength = 0; + + var currentCluster = clusters[start]; + + while (start > 0 && clusters[start - 1] == currentCluster) + { + start--; + } + + for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex) + { + if (_glyphClusters[lastIndex] != currentCluster) + { + break; + } + + if (GlyphAdvances.IsEmpty) + { + var glyph = GlyphIndices[lastIndex]; + + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + else + { + width += GlyphAdvances[lastIndex]; + } + + trailingLength++; + } + + return new CharacterHit(currentCluster, trailingLength); + } + + private Rect CalculateBounds() + { + var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale; + + var width = 0.0; + + if (GlyphAdvances.IsEmpty) + { + foreach (var glyph in GlyphIndices) + { + width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + } + } + else + { + foreach (var advance in GlyphAdvances) + { + width += advance; + } + } + + return new Rect(0, 0, width, height); + } + + private void Set(ref T field, T value) + { + if (_glyphRunImpl != null) + { + throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'"); + } + + field = value; + } + + private void Initialize(Rect? bounds) + { + if (GlyphIndices.Length == 0) + { + throw new InvalidOperationException(); + } + + var glyphCount = GlyphIndices.Length; + + if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount) + { + throw new InvalidOperationException(); + } + + _glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width); + + if (bounds.HasValue) + { + _bounds = bounds; + } + else + { + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; + + _bounds = new Rect(0, 0, width, height); + } + } + + void IDisposable.Dispose() + { + _glyphRunImpl?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs new file mode 100644 index 0000000000..22d6e20b34 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs @@ -0,0 +1,50 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Media +{ + public class GlyphRunDrawing : Drawing + { + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); + + public static readonly StyledProperty BaselineOriginProperty = + AvaloniaProperty.Register(nameof(BaselineOrigin)); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public GlyphRun GlyphRun + { + get => GetValue(GlyphRunProperty); + set => SetValue(GlyphRunProperty, value); + } + + public Point BaselineOrigin + { + get => GetValue(BaselineOriginProperty); + set => SetValue(BaselineOriginProperty, value); + } + + public override void Draw(DrawingContext context) + { + if (GlyphRun == null) + { + return; + } + + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + public override Rect GetBounds() + { + return GlyphRun?.Bounds ?? default; + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 3ba31f7e84..6468f701d6 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,11 +9,9 @@ namespace Avalonia.Media { public sealed class GlyphTypeface : IDisposable { - private static readonly IPlatformRenderInterface s_platformRenderInterface = - AvaloniaLocator.Current.GetService(); - - public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) + public GlyphTypeface(Typeface typeface) { + PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface); } public GlyphTypeface(IGlyphTypefaceImpl platformImpl) @@ -68,6 +66,11 @@ namespace Avalonia.Media /// public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + public bool IsFixedPitch => PlatformImpl.IsFixedPitch; + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Media/IImage.cs b/src/Avalonia.Visuals/Media/IImage.cs new file mode 100644 index 0000000000..aff2a9ddf9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IImage.cs @@ -0,0 +1,29 @@ +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Represents a raster or vector image. + /// + public interface IImage + { + /// + /// Gets the size of the image, in device independent pixels. + /// + Size Size { get; } + + /// + /// Draws the image to a . + /// + /// The drawing context. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode); + } +} diff --git a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs index 8dd75d2374..14ac4261dc 100644 --- a/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/Bitmap.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Imaging { @@ -94,9 +95,28 @@ namespace Avalonia.Media.Imaging PlatformImpl.Item.Save(fileName); } + /// + /// Saves the bitmap to a stream. + /// + /// The stream. public void Save(Stream stream) { PlatformImpl.Item.Save(stream); } + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + context.PlatformImpl.DrawBitmap( + PlatformImpl, + 1, + sourceRect, + destRect, + bitmapInterpolationMode); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs index 90b13088e1..4c3203a95b 100644 --- a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.Imaging /// /// Represents a bitmap image. /// - public interface IBitmap : IDisposable + public interface IBitmap : IImage, IDisposable { /// /// Gets the dots per inch (DPI) of the image. @@ -32,15 +32,6 @@ namespace Avalonia.Media.Imaging /// IRef PlatformImpl { get; } - /// - /// Gets the size of the image, in device independent pixels. - /// - /// - /// Note that Skia does not currently support reading the DPI of an image so this value - /// will equal on Skia. - /// - Size Size { get; } - /// /// Saves the bitmap to a file. /// diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index b55ca251a6..7d3f3bc4eb 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -1,12 +1,14 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; + namespace Avalonia.Media.Immutable { /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush + public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -47,6 +49,35 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + public bool Equals(ImmutableSolidColorBrush other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return Color == other.Color && Opacity == other.Opacity; + } + + public override bool Equals(object obj) + { + return obj is ImmutableSolidColorBrush other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode(); + } + } + + public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) + { + return !left.Equals(right); + } + /// /// Returns a string representation of the brush. /// diff --git a/src/Avalonia.Visuals/Media/MediaExtensions.cs b/src/Avalonia.Visuals/Media/MediaExtensions.cs index 95d17b454e..36bda5f483 100644 --- a/src/Avalonia.Visuals/Media/MediaExtensions.cs +++ b/src/Avalonia.Visuals/Media/MediaExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -16,24 +17,82 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// A vector with the X and Y scaling factors. - public static Vector CalculateScaling(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Vector CalculateScaling( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - double scaleX = 1; - double scaleY = 1; + var scaleX = 1.0; + var scaleY = 1.0; - if (stretch != Stretch.None) + bool isConstrainedWidth = !double.IsPositiveInfinity(destinationSize.Width); + bool isConstrainedHeight = !double.IsPositiveInfinity(destinationSize.Height); + + if ((stretch == Stretch.Uniform || stretch == Stretch.UniformToFill || stretch == Stretch.Fill) + && (isConstrainedWidth || isConstrainedHeight)) { - scaleX = destinationSize.Width / sourceSize.Width; - scaleY = destinationSize.Height / sourceSize.Height; + // Compute scaling factors for both axes + scaleX = MathUtilities.IsZero(sourceSize.Width) ? 0.0 : destinationSize.Width / sourceSize.Width; + scaleY = MathUtilities.IsZero(sourceSize.Height) ? 0.0 : destinationSize.Height / sourceSize.Height; - switch (stretch) + if (!isConstrainedWidth) + { + scaleX = scaleY; + } + else if (!isConstrainedHeight) + { + scaleY = scaleX; + } + else { - case Stretch.Uniform: - scaleX = scaleY = Math.Min(scaleX, scaleY); + // If not preserving aspect ratio, then just apply transform to fit + switch (stretch) + { + case Stretch.Uniform: + // Find minimum scale that we use for both axes + double minscale = scaleX < scaleY ? scaleX : scaleY; + scaleX = scaleY = minscale; + break; + + case Stretch.UniformToFill: + // Find maximum scale that we use for both axes + double maxscale = scaleX > scaleY ? scaleX : scaleY; + scaleX = scaleY = maxscale; + break; + + case Stretch.Fill: + // We already computed the fill scale factors above, so just use them + break; + } + } + + // Apply stretch direction by bounding scales. + // In the uniform case, scaleX=scaleY, so this sort of clamping will maintain aspect ratio + // In the uniform fill case, we have the same result too. + // In the fill case, note that we change aspect ratio, but that is okay + switch (stretchDirection) + { + case StretchDirection.UpOnly: + if (scaleX < 1.0) + scaleX = 1.0; + if (scaleY < 1.0) + scaleY = 1.0; + break; + + case StretchDirection.DownOnly: + if (scaleX > 1.0) + scaleX = 1.0; + if (scaleY > 1.0) + scaleY = 1.0; break; - case Stretch.UniformToFill: - scaleX = scaleY = Math.Max(scaleX, scaleY); + + case StretchDirection.Both: + break; + + default: break; } } @@ -47,10 +106,15 @@ namespace Avalonia.Media /// The stretch mode. /// The size of the destination viewport. /// The size of the source. + /// The stretch direction. /// The size of the stretched source. - public static Size CalculateSize(this Stretch stretch, Size destinationSize, Size sourceSize) + public static Size CalculateSize( + this Stretch stretch, + Size destinationSize, + Size sourceSize, + StretchDirection stretchDirection = StretchDirection.Both) { - return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize); + return sourceSize * stretch.CalculateScaling(destinationSize, sourceSize, stretchDirection); } } } diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 5ed16ca957..0fdc40c85c 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -23,7 +23,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty IsFilledProperty = + public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); private Points _points; diff --git a/src/Avalonia.Visuals/Media/StretchDirection.cs b/src/Avalonia.Visuals/Media/StretchDirection.cs new file mode 100644 index 0000000000..a4be26f6cd --- /dev/null +++ b/src/Avalonia.Visuals/Media/StretchDirection.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Media +{ + /// + /// Describes the type of scaling that can be used when scaling content. + /// + public enum StretchDirection + { + /// + /// Only scales the content upwards when the content is smaller than the available space. + /// If the content is larger, no scaling downwards is done. + /// + UpOnly, + + /// + /// Only scales the content downwards when the content is larger than the available space. + /// If the content is smaller, no scaling upwards is done. + /// + DownOnly, + + /// + /// Always stretches to fit the available space according to the stretch mode. + /// + Both, + } +} diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 7a70657ce0..8253d11ff1 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -28,6 +28,16 @@ namespace Avalonia.Media /// public abstract Matrix Value { get; } + /// + /// Parses a string. + /// + /// Six comma-delimited double values that describe the new . For details check + /// The . + public static Transform Parse(string s) + { + return new MatrixTransform(Matrix.Parse(s)); + } + /// /// Raises the event. /// @@ -35,5 +45,14 @@ namespace Avalonia.Media { Changed?.Invoke(this, EventArgs.Empty); } + + /// + /// Returns a String representing this transform matrix instance. + /// + /// The string representation. + public override string ToString() + { + return Value.ToString(); + } } } diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Visuals/Media/TransformGroup.cs index 3a47f40045..886a6479dd 100644 --- a/src/Avalonia.Visuals/Media/TransformGroup.cs +++ b/src/Avalonia.Visuals/Media/TransformGroup.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly AvaloniaProperty ChildrenProperty = + public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); public TransformGroup() diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index a6d5c8a43c..9a17bad7d2 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -13,8 +13,6 @@ namespace Avalonia.Media [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] public class Typeface : IEquatable { - public static readonly Typeface Default = new Typeface(FontFamily.Default); - private GlyphTypeface _glyphTypeface; /// @@ -50,6 +48,8 @@ namespace Avalonia.Media { } + public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + /// /// Gets the font family. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index f74c551fe0..7d142b0759 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -33,7 +33,7 @@ namespace Avalonia.Platform /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. @@ -61,12 +61,22 @@ namespace Avalonia.Platform void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// - /// Draws the outline of a rectangle. + /// Draws a rectangle with the specified Brush and Pen. /// - /// The pen. + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. - /// The corner radius. - void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0); /// /// Draws text. @@ -77,12 +87,12 @@ namespace Avalonia.Platform void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text); /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f); + /// The foreground. + /// The glyph run. + /// The baseline origin of the glyph run. + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 254b5d07d1..a8e6dcb29b 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -12,7 +13,7 @@ namespace Avalonia.Platform /// /// Gets the system's default font family's name. /// - string DefaultFontFamilyName { get; } + string GetDefaultFontFamilyName(); /// /// Get all installed fonts in the system. @@ -20,17 +21,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Get a typeface from specified parameters. - /// - /// The font family. - /// The font weight. - /// The font style. - /// - /// The typeface. - /// - Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - /// /// Tries to match a specified character to a typeface that supports specified font properties. /// @@ -39,10 +29,20 @@ namespace Avalonia.Platform /// The font style. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching font key. /// - /// The typeface. + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + + /// + /// Creates a glyph typeface. + /// + /// The typeface. + /// 0 + /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. /// - Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs new file mode 100644 index 0000000000..0f1359794a --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs @@ -0,0 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Platform +{ + /// + /// Actual implementation of a glyph run that stores platform dependent resources. + /// + public interface IGlyphRunImpl : IDisposable { } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs index 8c043a5129..5d6ff23c0a 100644 --- a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -47,6 +47,11 @@ namespace Avalonia.Platform /// int StrikethroughThickness { get; } + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + bool IsFixedPitch { get; } + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 5a0a7b2f19..7ae0eaf8f2 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -113,12 +113,17 @@ namespace Avalonia.Platform IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); /// - /// Creates a glyph typeface for specified typeface. + /// Creates a font manager implementation. /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + /// The font manager. + IFontManagerImpl CreateFontManager(); + + /// + /// Creates a platform implementation of a glyph run. + /// + /// The glyph run. + /// The glyph run's width. + /// + IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width); } } diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index d92f8b0fc4..27ac7a3026 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -175,7 +175,7 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The . + /// The . public static Point Parse(string s) { using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index 2e8fb16bc1..ebd0ba9351 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -177,5 +177,16 @@ namespace Avalonia unit); } } + + /// + /// Returns a String representing this RelativePoint instance. + /// + /// The string representation. + public override string ToString() + { + return _unit == RelativeUnit.Absolute ? + _point.ToString() : + string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); + } } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index d9a68b236a..b065079564 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -160,16 +160,23 @@ namespace Avalonia.Rendering /// public IEnumerable HitTest(Point p, IVisual root, Func filter) { - if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) - { - // When unit testing the renderLoop may be null, so update the scene manually. - UpdateScene(); - } + EnsureCanHitTest(); + //It's safe to access _scene here without a lock since //it's only changed from UI thread which we are currently on return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); } + /// + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + EnsureCanHitTest(); + + //It's safe to access _scene here without a lock since + //it's only changed from UI thread which we are currently on + return _scene?.Item.HitTestFirst(p, root, filter); + } + /// public void Paint(Rect rect) { @@ -235,6 +242,15 @@ namespace Avalonia.Rendering internal Scene UnitTestScene() => _scene.Item; + private void EnsureCanHitTest() + { + if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) + { + // When unit testing the renderLoop may be null, so update the scene manually. + UpdateScene(); + } + } + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) @@ -445,7 +461,7 @@ namespace Avalonia.Rendering foreach (var r in _dirtyRectsDisplay) { var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); - context.FillRectangle(brush, r.Rect); + context.DrawRectangle(brush,null, r.Rect); } } @@ -469,11 +485,11 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); + context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { - context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); + context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); } if (layer.GeometryClip != null) @@ -485,7 +501,7 @@ namespace Avalonia.Rendering if (_overlay != null) { var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); - context.DrawImage(_overlay, 0.5, sourceRect, clientRect); + context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); } if (DrawFps) diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs index 7199053b08..4c15de0312 100644 --- a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs +++ b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + namespace Avalonia.Rendering { /// @@ -9,4 +13,13 @@ namespace Avalonia.Rendering { bool HitTest(Point point); } + + public static class CustomSimpleHitTestExtensions + { + public static bool HitTestCustom(this IVisual visual, Point point) + => (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; + + public static bool HitTestCustom(this IEnumerable children, Point point) + => children.Any(ctrl => ctrl.HitTestCustom(point)); + } } diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 9ad7186dca..7dc2dfde88 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -50,6 +50,18 @@ namespace Avalonia.Rendering /// The visuals at the specified point, topmost first. IEnumerable HitTest(Point p, IVisual root, Func filter); + /// + /// Hit tests a location to find first visual at the specified point. + /// + /// The point, in client coordinates. + /// The root of the subtree to search. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the specified point, topmost first. + IVisual HitTestFirst(Point p, IVisual root, Func filter); + /// /// Informs the renderer that the z-ordering of a visual's children has changed. /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 68d56eeedd..aade57b7f0 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -164,6 +164,11 @@ namespace Avalonia.Rendering return HitTest(root, p, filter); } + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + return HitTest(root, p, filter).FirstOrDefault(); + } + /// public void RecalculateChildren(IVisual visual) => AddDirty(visual); @@ -307,7 +312,9 @@ namespace Avalonia.Rendering if (!child.ClipToBounds || clipRect.Intersects(childBounds)) { - var childClipRect = clipRect.Translate(-childBounds.Position); + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; Render(context, child, childClipRect); } else diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index e341f02901..1e7b5c2923 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; @@ -19,7 +18,7 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface, + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), FontSize = s_fontSize }; } @@ -51,7 +50,7 @@ namespace Avalonia.Rendering var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); context.Transform = Matrix.Identity; - context.FillRectangle(Brushes.Black, rect); + context.DrawRectangle(Brushes.Black,null, rect); context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 3af56f5215..b362321745 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -115,7 +115,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var next = NextDrawAs(); @@ -130,7 +130,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) { // This method is currently only used to composite layers so shouldn't be called here. throw new NotSupportedException(); @@ -152,13 +152,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, null, pen, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY)) { - Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush))); + Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, CreateChildScene(brush))); } else { @@ -191,20 +191,20 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - var next = NextDrawAs(); + var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, brush, null, rect, cornerRadius)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) { - Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush))); + Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground))); } + else { ++_drawOperationindex; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) { throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs new file mode 100644 index 0000000000..b3c4fdbac0 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -0,0 +1,91 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; + +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a glyph run draw. + /// + internal class GlyphRunNode : BrushDrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The foreground brush. + /// The glyph run to draw. + /// The baseline origin of the glyph run. + /// Child scenes for drawing visual brushes. + public GlyphRunNode( + Matrix transform, + IBrush foreground, + GlyphRun glyphRun, + Point baselineOrigin, + IDictionary childScenes = null) + : base(glyphRun.Bounds, transform, null) + { + Transform = transform; + Foreground = foreground?.ToImmutable(); + GlyphRun = glyphRun; + BaselineOrigin = baselineOrigin; + ChildScenes = childScenes; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the foreground brush. + /// + public IBrush Foreground { get; } + + /// + /// Gets the glyph run to draw. + /// + public GlyphRun GlyphRun { get; } + + /// + /// Gets the baseline origin. + /// + public Point BaselineOrigin { get; set; } + + /// + public override IDictionary ChildScenes { get; } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The foreground of the other draw operation. + /// The glyph run of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + { + return transform == Transform && + Equals(foreground, Foreground) && + Equals(glyphRun, GlyphRun); + } + + /// + public override bool HitTest(Point p) => Bounds.Contains(p); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index e1bdcaab3b..054f33c95d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { context.Transform = Transform; - context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 0f3581b84c..b40afe78c5 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -21,14 +22,16 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The rectangle corner radius. + /// The radius in the Y dimension of the rounded corners. + /// The radius in the X dimension of the rounded corners. /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, IBrush brush, IPen pen, Rect rect, - float cornerRadius, + double radiusX, + double radiusY, IDictionary childScenes = null) : base(rect, transform, pen) { @@ -36,7 +39,8 @@ namespace Avalonia.Rendering.SceneGraph Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; - CornerRadius = cornerRadius; + RadiusX = radiusX; + RadiusY = radiusY; ChildScenes = childScenes; } @@ -61,9 +65,14 @@ namespace Avalonia.Rendering.SceneGraph public Rect Rect { get; } /// - /// Gets the rectangle corner radius. + /// The radius in the X dimension of the rounded corners. /// - public float CornerRadius { get; } + public double RadiusX { get; } + + /// + /// The radius in the Y dimension of the rounded corners. + /// + public double RadiusY { get; } /// public override IDictionary ChildScenes { get; } @@ -75,19 +84,21 @@ namespace Avalonia.Rendering.SceneGraph /// The fill of the other draw operation. /// The stroke of the other draw operation. /// The rectangle of the other draw operation. - /// The rectangle corner radius of the other draw operation. + /// + /// /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius) + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect == Rect && - cornerRadius == CornerRadius; + Equals(brush, Brush) && + Equals(Pen, pen) && + rect == Rect && + Math.Abs(radiusX - RadiusX) < double.Epsilon && + Math.Abs(radiusY - RadiusY) < double.Epsilon; } /// @@ -95,15 +106,7 @@ namespace Avalonia.Rendering.SceneGraph { context.Transform = Transform; - if (Brush != null) - { - context.FillRectangle(Brush, Rect, CornerRadius); - } - - if (Pen != null) - { - context.DrawRectangle(Pen, Rect, CornerRadius); - } + context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY); } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 1afc096c98..70355e6833 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using Avalonia.Collections.Pooled; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -128,7 +130,20 @@ namespace Avalonia.Rendering.SceneGraph public IEnumerable HitTest(Point p, IVisual root, Func filter) { var node = FindNode(root); - return (node != null) ? HitTest(node, p, null, filter) : Enumerable.Empty(); + return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty(); + } + + /// + /// Gets the visual at a point in the scene. + /// + /// The point. + /// The root of the subtree to search. + /// A filter. May be null. + /// The visual at the specified point. + public IVisual HitTestFirst(Point p, IVisual root, Func filter) + { + var node = FindNode(root); + return (node != null) ? HitTestFirst(node, p, filter) : null; } /// @@ -158,38 +173,157 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private IEnumerable HitTest(IVisualNode node, Point p, Rect? clip, Func filter) + private IVisual HitTestFirst(IVisualNode root, Point p, Func filter) { - if (filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + using var enumerator = new HitTestEnumerator(root, filter, p, Root); + + enumerator.MoveNext(); + + return enumerator.Current; + } + + private class HitTestEnumerable : IEnumerable + { + private readonly IVisualNode _root; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private readonly Point _point; + + public HitTestEnumerable(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) { - var clipped = false; + _root = root; + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; + } - if (node.ClipToBounds) - { - clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); - clipped = !clip.Value.Contains(p); - } + public IEnumerator GetEnumerator() + { + return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); + } - if (node.GeometryClip != null) - { - var controlPoint = Root.Visual.TranslatePoint(p, node.Visual); - clipped = !node.GeometryClip.FillContains(controlPoint.Value); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private struct HitTestEnumerator : IEnumerator + { + private readonly PooledStack _nodeStack; + private readonly Func _filter; + private readonly IVisualNode _sceneRoot; + private IVisual _current; + private readonly Point _point; + + public HitTestEnumerator(IVisualNode root, Func filter, Point point, IVisualNode sceneRoot) + { + _nodeStack = new PooledStack(); + _nodeStack.Push(new Entry(root, false, null, true)); + + _filter = filter; + _point = point; + _sceneRoot = sceneRoot; - if (!clipped) + _current = null; + } + + public bool MoveNext() + { + while (_nodeStack.Count > 0) { - for (var i = node.Children.Count - 1; i >= 0; --i) + (var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); + + if (wasVisited && isRoot) { - foreach (var h in HitTest(node.Children[i], p, clip, filter)) + break; + } + + var children = node.Children; + int childCount = children.Count; + + if (childCount == 0 || wasVisited) + { + if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point)) { - yield return h; + _current = node.Visual; + + return true; } } + else if (FilterAndClip(node, ref clip)) + { + _nodeStack.Push(new Entry(node, true, null)); + + for (var i = 0; i < childCount; i++) + { + _nodeStack.Push(new Entry(children[i], false, clip)); + } + } + } + + return false; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public IVisual Current => _current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _nodeStack.Dispose(); + } - if (node.HitTest(p)) + private bool FilterAndClip(IVisualNode node, ref Rect? clip) + { + if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) + { + var clipped = false; + + if (node.ClipToBounds) { - yield return node.Visual; + clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); + clipped = !clip.Value.Contains(_point); } + + if (node.GeometryClip != null) + { + var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); + clipped = !node.GeometryClip.FillContains(controlPoint.Value); + } + + return !clipped; + } + + return false; + } + + private readonly struct Entry + { + public readonly bool WasVisited; + public readonly bool IsRoot; + public readonly IVisualNode Node; + public readonly Rect? Clip; + + public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) + { + Node = node; + WasVisited = wasVisited; + IsRoot = isRoot; + Clip = clip; + } + + public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) + { + wasVisited = WasVisited; + isRoot = IsRoot; + node = Node; + clip = Clip; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index d2a9e0a673..93d68e5230 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -280,8 +280,13 @@ namespace Avalonia.Rendering.SceneGraph /// public bool HitTest(Point p) { - foreach (var operation in DrawOperations) + var drawOperations = DrawOperations; + var drawOperationsCount = drawOperations.Count; + + for (var i = 0; i < drawOperationsCount; i++) { + var operation = drawOperations[i]; + if (operation?.Item?.HitTest(p) == true) { return true; diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs new file mode 100644 index 0000000000..c54ccc8ef1 --- /dev/null +++ b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs @@ -0,0 +1,154 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Utility +{ + /// + /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. + /// + /// The type of elements in the slice. + public readonly struct ReadOnlySlice : IReadOnlyList + { + public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } + + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length) + { + Buffer = buffer; + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets a value that indicates whether this instance of is Empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// The buffer. + /// + public ReadOnlyMemory Buffer { get; } + + public T this[int index] => Buffer.Span[Start + index]; + + /// + /// Returns a span of the underlying buffer. + /// + /// The of the underlying buffer. + public ReadOnlySpan AsSpan() + { + return Buffer.Span.Slice(Start, Length); + } + + /// + /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. + /// + /// The start of the sub slice. + /// The length of the sub slice. + /// A that contains the specified number of elements from the specified start. + public ReadOnlySlice AsSlice(int start, int length) + { + if (start < 0 || start >= Length) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (Start + start > End) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start + start, length); + } + + /// + /// Returns a specified number of contiguous elements from the start of the slice. + /// + /// The number of elements to return. + /// A that contains the specified number of elements from the start of this slice. + public ReadOnlySlice Take(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start, length); + } + + /// + /// Bypasses a specified number of elements in the slice and then returns the remaining elements. + /// + /// The number of elements to skip before returning the remaining elements. + /// A that contains the elements that occur after the specified index in this slice. + public ReadOnlySlice Skip(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start + length, Length - length); + } + + /// + /// Returns an enumerator for the slice. + /// + public ImmutableReadOnlyListStructEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + int IReadOnlyCollection.Count => Length; + + T IReadOnlyList.this[int index] => this[index]; + + public static implicit operator ReadOnlySlice(T[] array) + { + return new ReadOnlySlice(array); + } + + public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) + { + return new ReadOnlySlice(memory); + } + } +} diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 576d2daaaa..d99fbe8e65 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using Avalonia.Animation.Animators; +using Avalonia.Utilities; using JetBrains.Annotations; namespace Avalonia @@ -85,6 +86,22 @@ namespace Avalonia public static Vector operator /(Vector vector, double scale) => Divide(vector, scale); + /// + /// Parses a string. + /// + /// The string. + /// The . + public static Vector Parse(string s) + { + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) + { + return new Vector( + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); + } + } + /// /// Length of the vector /// @@ -166,9 +183,9 @@ namespace Avalonia } /// - /// Returns the string representation of the point. + /// Returns the string representation of the vector. /// - /// The string representation of the point. + /// The string representation of the vector. public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index d615bea450..ef7a254f86 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; @@ -121,7 +120,7 @@ namespace Avalonia { var visualChildren = new AvaloniaList(); visualChildren.ResetBehavior = ResetBehavior.Remove; - visualChildren.Validate = ValidateVisualChild; + visualChildren.Validate = visual => ValidateVisualChild(visual); visualChildren.CollectionChanged += VisualChildrenChanged; VisualChildren = visualChildren; } @@ -173,7 +172,22 @@ namespace Avalonia /// public bool IsEffectivelyVisible { - get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + get + { + IVisual node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } } /// @@ -556,7 +570,7 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var root = this.FindAncestorOfType(); var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 567b676b1e..f32d67db1e 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -14,7 +14,7 @@ namespace Avalonia.VisualTree public static class VisualExtensions { /// - /// Calculates the distance from a visual's . + /// Calculates the distance from a visual's ancestor. /// /// The visual. /// The ancestor visual. @@ -30,13 +30,39 @@ namespace Avalonia.VisualTree while (visual != null && visual != ancestor) { - ++result; visual = visual.VisualParent; + + result++; } return visual != null ? result : -1; } + /// + /// Calculates the distance from a visual's root. + /// + /// The visual. + /// + /// The number of steps from the visual to the root. + /// + public static int CalculateDistanceFromRoot(IVisual visual) + { + Contract.Requires(visual != null); + + var result = 0; + + visual = visual?.VisualParent; + + while (visual != null) + { + visual = visual.VisualParent; + + result++; + } + + return result; + } + /// /// Tries to get the first common ancestor of two visuals. /// @@ -47,8 +73,53 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + void GoUpwards(ref IVisual node, int count) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + } + } + + // We want to find lowest node first, then make sure that both nodes are at the same height. + // By doing that we can sometimes find out that other node is our lowest common ancestor. + var firstHeight = CalculateDistanceFromRoot(visual); + var secondHeight = CalculateDistanceFromRoot(target); + + if (firstHeight > secondHeight) + { + GoUpwards(ref visual, firstHeight - secondHeight); + } + else + { + GoUpwards(ref target, secondHeight - firstHeight); + } + + if (visual == target) + { + return visual; + } + + while (visual != null && target != null) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + } + + return null; } /// @@ -69,6 +140,57 @@ namespace Avalonia.VisualTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// First ancestor of given type. + public static T FindAncestorOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + IVisual parent = includeSelf ? visual : visual.VisualParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.VisualParent; + } + + return null; + } + + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// First descendant of given type. + public static T FindDescendantOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + if (includeSelf && visual is T result) + { + return result; + } + + return FindDescendantOfTypeCore(visual); + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -91,12 +213,37 @@ namespace Avalonia.VisualTree /// /// The root visual to test. /// The point. - /// The visuals at the requested point. + /// The visual at the requested point. public static IVisual GetVisualAt(this IVisual visual, Point p) { Contract.Requires(visual != null); - return visual.GetVisualsAt(p).FirstOrDefault(); + return visual.GetVisualAt(p, x => x.IsVisible); + } + + /// + /// Gets the first visual in the visual tree whose bounds contain a point. + /// + /// The root visual to test. + /// The point. + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The visual at the requested point. + public static IVisual GetVisualAt(this IVisual visual, Point p, Func filter) + { + Contract.Requires(visual != null); + + var root = visual.GetVisualRoot(); + var rootPoint = visual.TranslatePoint(p, root); + + if (rootPoint.HasValue) + { + return root.Renderer.HitTestFirst(rootPoint.Value, visual, filter); + } + + return null; } /// @@ -249,6 +396,31 @@ namespace Avalonia.VisualTree .Select(x => x.Element); } + private static T FindDescendantOfTypeCore(IVisual visual) where T : class + { + var visualChildren = visual.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; + } + private class ZOrderElement : IComparable { public IVisual Element { get; set; } diff --git a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs index eaca9ed9d7..1e0662c17a 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Reactive.Linq; -using System.Reflection; using Avalonia.Reactive; namespace Avalonia.VisualTree @@ -50,7 +48,7 @@ namespace Avalonia.VisualTree if (_relativeTo.IsAttachedToVisualTree) { return _relativeTo.GetVisualAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .Where(x => _ancestorType?.IsAssignableFrom(x.GetType()) ?? true) .ElementAtOrDefault(_ancestorLevel); } else diff --git a/src/Avalonia.X11/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index c3a2fd2050..714a592f2b 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -84,8 +84,24 @@ namespace Avalonia.X11.Glx [GlEntryPoint("glGetError")] public GlGetError GetError { get; } - public GlxInterface() : base(GlxGetProcAddress) + public GlxInterface() : base(SafeGetProcAddress) { } + + // Ignores egl functions. + // On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions. + // This makes Skia try to load some data from EGL, + // which can then cause segmentation faults because they return garbage. + public static IntPtr SafeGetProcAddress(string proc, bool optional) + { + if (proc.StartsWith("egl", StringComparison.InvariantCulture)) + { + return IntPtr.Zero; + } + + return GlxConverted(proc, optional); + } + + private static readonly Func GlxConverted = ConvertNative(GlxGetProcAddress); } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 04f2a7137c..22eb0792e8 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -87,7 +87,7 @@ namespace Avalonia.X11.Glx ImmediateContext.MakeCurrent(); var err = Glx.GetError(); - GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); + GlInterface = new GlInterface(GlxInterface.SafeGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); if (GlInterface.Renderer == null) diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index cb9d3389e4..a431ffcc1a 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -79,7 +79,7 @@ namespace Avalonia.X11 atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE}) .ToArray(); XChangeProperty(_x11.Display, window, property, - target, 32, PropertyMode.Replace, atoms, atoms.Length); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) diff --git a/src/Avalonia.X11/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs index 00288f300d..94f930e9ec 100644 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ b/src/Avalonia.X11/X11Framebuffer.cs @@ -14,6 +14,10 @@ namespace Avalonia.X11 public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) { + // HACK! Please fix renderer, should never ask for 0x0 bitmap. + width = Math.Max(1, width); + height = Math.Max(1, height); + _display = display; _xid = xid; _depth = depth; diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index f0e75536d0..093f2b12c1 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -59,7 +59,7 @@ namespace Avalonia.X11 } using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) - ctx.DrawImage(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), + ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 6ba562bb69..8b531bd9c5 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -44,7 +44,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Control)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 32460fed86..919abae243 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -102,7 +102,7 @@ namespace Avalonia.X11 valueMask |= SetWindowValuemask.ColorMap; } - int defaultWidth = 300, defaultHeight = 200; + int defaultWidth = 0, defaultHeight = 0; if (!_popup && Screen != null) { @@ -117,6 +117,10 @@ namespace Avalonia.X11 } } + // check if the calculated size is zero then compensate to hardcoded resolution + defaultWidth = Math.Max(defaultWidth, 300); + defaultHeight = Math.Max(defaultHeight, 200); + _handle = XCreateWindow(_x11.Display, _x11.RootWindow, 10, 10, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, @@ -133,7 +137,7 @@ namespace Avalonia.X11 _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); - _realSize = new PixelSize(300, 200); + _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask | XEventMask.ResizeRedirectMask @@ -351,10 +355,17 @@ namespace Avalonia.X11 { if (ActivateTransientChildIfNeeded()) return; - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown - : RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonDown, + 2 => RawPointerEventType.MiddleButtonDown, + 3 => RawPointerEventType.RightButtonDown, + 8 => RawPointerEventType.XButton1Down, + 9 => RawPointerEventType.XButton2Down + }, + ref ev, ev.ButtonEvent.state); else { var delta = ev.ButtonEvent.button == 4 @@ -372,10 +383,17 @@ namespace Avalonia.X11 } else if (ev.type == XEventName.ButtonRelease) { - if (ev.ButtonEvent.button < 4) - MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp - : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp - : RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state); + if (ev.ButtonEvent.button < 4 || ev.ButtonEvent.button == 8 || ev.ButtonEvent.button == 9) + MouseEvent( + ev.ButtonEvent.button switch + { + 1 => RawPointerEventType.LeftButtonUp, + 2 => RawPointerEventType.MiddleButtonUp, + 3 => RawPointerEventType.RightButtonUp, + 8 => RawPointerEventType.XButton1Up, + 9 => RawPointerEventType.XButton2Up + }, + ref ev, ev.ButtonEvent.state); } else if (ev.type == XEventName.ConfigureNotify) { @@ -586,8 +604,12 @@ namespace Avalonia.X11 rv |= RawInputModifiers.LeftMouseButton; if (state.HasFlag(XModifierMask.Button2Mask)) rv |= RawInputModifiers.RightMouseButton; - if (state.HasFlag(XModifierMask.Button2Mask)) + if (state.HasFlag(XModifierMask.Button3Mask)) rv |= RawInputModifiers.MiddleMouseButton; + if (state.HasFlag(XModifierMask.Button4Mask)) + rv |= RawInputModifiers.XButton1MouseButton; + if (state.HasFlag(XModifierMask.Button5Mask)) + rv |= RawInputModifiers.XButton2MouseButton; if (state.HasFlag(XModifierMask.ShiftMask)) rv |= RawInputModifiers.Shift; if (state.HasFlag(XModifierMask.ControlMask)) diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index e37ed39bee..ac14efe133 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -240,11 +240,15 @@ namespace Avalonia.X11 if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease) { var down = ev.Type == XiEventType.XI_ButtonPress; - var type = - ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) - : ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) - : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp) - : (RawPointerEventType?)null; + var type = ev.Button switch + { + 1 => down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp, + 2 => down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp, + 3 => down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp, + 8 => down ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton1Up, + 9 => down ? RawPointerEventType.XButton2Down : RawPointerEventType.XButton2Up, + _ => (RawPointerEventType?)null + }; if (type.HasValue) client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); @@ -283,12 +287,14 @@ namespace Avalonia.X11 var buttons = ev->buttons.Mask; if (XIMaskIsSet(buttons, 1)) Modifiers |= RawInputModifiers.LeftMouseButton; - if (XIMaskIsSet(buttons, 2)) Modifiers |= RawInputModifiers.MiddleMouseButton; - if (XIMaskIsSet(buttons, 3)) Modifiers |= RawInputModifiers.RightMouseButton; + if (XIMaskIsSet(buttons, 8)) + Modifiers |= RawInputModifiers.XButton1MouseButton; + if (XIMaskIsSet(buttons, 9)) + Modifiers |= RawInputModifiers.XButton2MouseButton; } Valuators = new Dictionary(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a466714136..c77ccd64f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions StringFormat = StringFormat, RelativeSource = RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + TargetNullValue = TargetNullValue, NameScope = new WeakReference(serviceProvider.GetService()) }; } @@ -52,6 +53,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = context.GetFirstParent(); + if(anchor is null) + { + // Try to find IDataContextProvider, this was added to allow us to find + // a datacontext for Application class when using NativeMenuItems. + anchor = context.GetFirstParent(); + } + // If a control was not found, then try to find the highest-level style as the XAML // file could be a XAML file containing only styles. return anchor ?? @@ -79,5 +87,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string StringFormat { get; set; } public RelativeSource RelativeSource { get; set; } + + public object TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 3525628a79..0d56942645 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude :IResourceProvider + public class ResourceInclude : IResourceNode, ISetResourceParent { + private IResourceNode _parent; private Uri _baseUri; private IResourceDictionary _loaded; @@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + (_loaded as ISetResourceParent)?.SetParent(this); + _loaded.ResourcesChanged += ResourcesChanged; + if (_loaded.HasResources) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); @@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions /// bool IResourceProvider.HasResources => Loaded.HasResources; + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// bool IResourceProvider.TryGetResource(object key, out object value) { return Loaded.TryGetResource(key, out value); } + /// + void ISetResourceParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The ResourceInclude already has a parent."); + } + + _parent = parent; + } + + /// + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + { + (_loaded as ISetResourceParent)?.ParentResourcesChanged(e); + } + public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { var tdc = (ITypeDescriptorContext)serviceProvider; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 7acee50d80..41eab79ed8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle, ISetStyleParent + public class StyleInclude : IStyle, ISetResourceParent { private Uri _baseUri; private IStyle _loaded; @@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); - (_loaded as ISetStyleParent)?.SetParent(this); + (_loaded as ISetResourceParent)?.SetParent(this); } return _loaded; @@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); /// - void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) { - (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + (Loaded as ISetResourceParent)?.ParentResourcesChanged(e); } /// - void ISetStyleParent.SetParent(IResourceNode parent) + void ISetResourceParent.SetParent(IResourceNode parent) { if (_parent != null && parent != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index c81a3718a7..d56435b4f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -28,7 +27,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 0e2a131afd..0e95af674b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -2,12 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Data; using Avalonia.Markup.Parsers; using Avalonia.Metadata; @@ -34,7 +31,7 @@ namespace Avalonia.Markup.Xaml.Templates } else { - return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); + return DataType.IsInstanceOfType(data); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 63c8b1c074..ebe4035ed6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -99,7 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); - Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index aab43bbd6f..40386924c3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn)) { - targetType = tn.Type; + targetType = tn.Value; } else { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index ad9915e193..4c4b6cf8ff 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3 +Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218 diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index dbe5800e55..b4545f792e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -26,6 +26,7 @@ namespace Avalonia.Data public Binding() { FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -60,6 +61,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -209,6 +215,7 @@ namespace Avalonia.Data observer, targetType, fallback, + TargetNullValue, converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); @@ -224,9 +231,9 @@ namespace Avalonia.Data { Contract.Requires(target != null); - if (!(target is IStyledElement)) + if (!(target is IDataContextProvider)) { - target = anchor as IStyledElement; + target = anchor as IDataContextProvider; if (target == null) { diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 29945e25c3..4325ad8a74 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -37,6 +37,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -57,6 +62,12 @@ namespace Avalonia.Data /// public string StringFormat { get; set; } + public MultiBinding() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -102,6 +113,11 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); + if (converted == null) + { + converted = TargetNullValue; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 8a5a725594..a23c31ca64 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -75,7 +75,7 @@ namespace Avalonia.Shared.PlatformSupport lock (_btlock) Backtraces.Remove(_backtrace); #endif - _plat.Free(_address, Size); + _plat?.Free(_address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true; _address = IntPtr.Zero; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 47e651ce91..1c05f8ac9f 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); @@ -146,10 +146,10 @@ namespace Avalonia.Skia } /// - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); - DrawImage(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); + DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); } @@ -184,37 +184,40 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { - using (var paint = CreatePaint(pen, rect.Size)) - { - var rc = rect.ToSKRect(); + var rc = rect.ToSKRect(); + var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; - if (Math.Abs(cornerRadius) < float.Epsilon) - { - Canvas.DrawRect(rc, paint.Paint); - } - else + if (brush != null) + { + using (var paint = CreatePaint(brush, rect.Size)) { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } } - } - /// - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) - { - using (var paint = CreatePaint(brush, rect.Size)) + if (pen?.Brush != null) { - var rc = rect.ToSKRect(); - - if (Math.Abs(cornerRadius) < float.Epsilon) - { - Canvas.DrawRect(rc, paint.Paint); - } - else + using (var paint = CreatePaint(pen, rect.Size)) { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + if (isRounded) + { + Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + } } } @@ -229,6 +232,20 @@ namespace Avalonia.Skia } } + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) + { + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + paint.ApplyTo(glyphRunImpl.Paint); + + Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, + (float)baselineOrigin.Y, glyphRunImpl.Paint); + } + } + /// public IRenderTargetBitmapImpl CreateLayer(Size size) { @@ -420,7 +437,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage( + context.DrawBitmap( RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, sourceRect, diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 03de82178a..60d6ecaabc 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,9 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -13,13 +15,11 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - public FontManagerImpl() + public string GetDefaultFontFamilyName() { - DefaultFontFamilyName = SKTypeface.Default.FamilyName; + return SKTypeface.Default.FamilyName; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) @@ -30,53 +30,89 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; - } + [ThreadStatic] private static string[] t_languageTagBuffer; - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - var fontFamilyName = FontFamily.Default.Name; - if (culture == null) { culture = CultureInfo.CurrentUICulture; } + if (t_languageTagBuffer == null) + { + t_languageTagBuffer = new string[2]; + } + + t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; + t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; + if (fontFamily != null) { foreach (var familyName in fontFamily.FamilyNames) { var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontFamilyName = familyName; + fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); - break; + return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { - fontFamilyName = skTypeface.FamilyName; + fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + + return true; } } - return GetTypeface(fontFamilyName, fontWeight, fontStyle); + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + var skTypeface = SKTypeface.Default; + + if (typeface.FontFamily.Key == null) + { + var defaultName = SKTypeface.Default.FamilyName; + + foreach (var familyName in typeface.FontFamily.FamilyNames) + { + skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + if (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal) && + defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) + { + continue; + } + + break; + } + } + else + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + } + + return new GlyphTypefaceImpl(skTypeface); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a9358cb458..8effb94ca9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -29,7 +29,7 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; _paint = new SKPaint { @@ -38,7 +38,7 @@ namespace Avalonia.Skia IsAntialias = true, LcdRenderText = true, SubpixelText = true, - Typeface = entry.SKTypeface, + Typeface = glyphTypeface.Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() }; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs new file mode 100644 index 0000000000..e0f62d6085 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + public class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + { + Paint = paint; + TextBlob = textBlob; + } + + /// + /// Gets the paint to draw with. + /// + public SKPaint Paint { get; } + + /// + /// Gets the text blob to draw. + /// + public SKTextBlob TextBlob { get; } + + void IDisposable.Dispose() + { + TextBlob.Dispose(); + Paint.Dispose(); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index e46f766255..bb2650a5c6 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -14,9 +14,9 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface) { - Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface; + Typeface = typeface; Face = new Face(GetTable) { @@ -61,6 +61,8 @@ namespace Avalonia.Skia { StrikethroughThickness = strikethroughThickness; } + + IsFixedPitch = Typeface.IsFixedPitch; } public Face Face { get; } @@ -81,7 +83,6 @@ namespace Avalonia.Skia /// public int LineGap { get; } - //ToDo: Get these values from HarfBuzz /// public int UnderlinePosition { get; } @@ -94,6 +95,9 @@ namespace Avalonia.Skia /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ee0cfb2f06..05c3bbdaa0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -18,9 +17,6 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -60,7 +56,7 @@ namespace Avalonia.Skia Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); @@ -155,9 +151,95 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); + } + + /// + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var count = glyphRun.GlyphIndices.Length; + + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var typeface = glyphTypeface.Typeface; + + var paint = new SKPaint + { + TextSize = (float)glyphRun.FontRenderingEmSize, + Typeface = typeface, + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + using (var textBlobBuilder = new SKTextBlobBuilder()) + { + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + + if (glyphRun.GlyphOffsets.IsEmpty) + { + width = 0; + + var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); + + if (!glyphTypeface.IsFixedPitch) + { + var positions = buffer.GetPositionSpan(); + + for (var i = 0; i < count; i++) + { + positions[i] = (float)width; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + width += glyphRun.GlyphAdvances[i]; + } + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan()); + } + else + { + var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; + + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } + } + + buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan()); + + width = currentX; + } + + var textBlob = textBlobBuilder.Build(); + + return new GlyphRunImpl(paint, textBlob); + } } } } diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6aabfae39 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")] diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 577567a8a1..d1c1961a8a 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -5,58 +5,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; +using Avalonia.Media.Fonts; +using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) + public void AddTypeface(FontKey key, SKTypeface typeface) { - if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) - { - fontFamily = new ConcurrentDictionary(); - - _fontFamilies.TryAdd(familyName, fontFamily); - } - - fontFamily.TryAdd(key, entry); + _typefaces.TryAdd(key, typeface); } - public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) + public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - var key = new FontKey(fontWeight, fontStyle); + var key = new FontKey(fontFamily, fontWeight, fontStyle); - return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? - fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : - new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default); + return GetNearestMatch(_typefaces, key); } - private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - var keys = fontFamily.Keys.Where( + if (typefaces.ContainsKey(key)) + { + return typefaces[key]; + } + + var keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } - key = keys.FirstOrDefault(); + if (keys.Length == 0) + { + return SKTypeface.Default; + } - fontFamily.TryGetValue(key, out var entry); + key = keys[0]; - return entry; + return typefaces[key]; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4bb42c7118..71edae26df 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,13 +45,11 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var skTypeface = SKTypeface.FromStream(assetStream); + var typeface = SKTypeface.FromStream(assetStream); - var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); - var entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); + typeFaceCollection.AddTypeface(key, typeface); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index ce3aef755b..f16e967f42 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,11 +25,6 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); - - var fontManager = new FontManagerImpl(); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs deleted file mode 100644 index 1c2b855032..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Concurrent; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - /// - /// Cache for Skia typefaces. - /// - internal static class TypefaceCache - { - private static readonly ConcurrentDictionary> s_cache = - new ConcurrentDictionary>(); - - public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - if (fontFamily.Key != null) - { - return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) - .Get(fontFamily.Name, fontWeight, fontStyle); - } - - var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - - var key = new FontKey(fontWeight, fontStyle); - - if (typefaceCollection.TryGetValue(key, out var entry)) - { - return entry; - } - - var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - - var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - - entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typefaceCollection[key] = entry; - - return entry; - } - } -} diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs deleted file mode 100644 index ef9f889819..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class TypefaceCollectionEntry - { - public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface) - { - Typeface = typeface; - SKTypeface = skTypeface; - } - public Typeface Typeface { get; } - public SKTypeface SKTypeface { get; } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1bda5157a5..a2bedf3190 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Platform; +using SharpDX.DirectWrite; +using GlyphRun = Avalonia.Media.GlyphRun; +using TextAlignment = Avalonia.Media.TextAlignment; namespace Avalonia { @@ -28,8 +31,6 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -109,7 +110,6 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -194,9 +194,57 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, data, size, dpi, stride); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); + } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl; + + var glyphCount = glyphRun.GlyphIndices.Length; + + var run = new SharpDX.DirectWrite.GlyphRun + { + FontFace = glyphTypeface.FontFace, + FontSize = (float)glyphRun.FontRenderingEmSize + }; + + var indices = new short[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + indices[i] = (short)glyphRun.GlyphIndices[i]; + } + + run.Indices = indices; + + run.Advances = new float[glyphCount]; + + width = 0; + + for (var i = 0; i < glyphCount; i++) + { + run.Advances[i] = (float)glyphRun.GlyphAdvances[i]; + width += run.Advances[i]; + } + + run.Offsets = new GlyphOffset[glyphCount]; + + for (var i = 0; i < glyphCount; i++) + { + var offset = glyphRun.GlyphOffsets[i]; + + run.Offsets[i] = new GlyphOffset + { + AdvanceOffset = (float)offset.X, + AscenderOffset = (float)offset.Y + }; + } + + return new GlyphRunImpl(run); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index b455c4fbee..78bf25d607 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); + int index; - foreach (var familyName in fontFamily.FamilyNames) + foreach (var name in fontFamily.FamilyNames) { - if (fontCollection.FindFamilyName(familyName, out var index)) + if (fontCollection.FindFamilyName(name, out index)) { return fontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, @@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media } } - InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + InstalledFontCollection.FindFamilyName("Segoe UI", out index); - return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, FontStretch.Normal, (FontStyle)typeface.Style); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 39d801eb2f..81d869f3b8 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) { @@ -149,7 +149,7 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) @@ -230,34 +230,64 @@ namespace Avalonia.Direct2D1.Media } } - /// - /// Draws the outline of a rectangle. - /// - /// The pen. - /// The rectangle bounds. - /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius) + /// + public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY) { - using (var brush = CreateBrush(pen.Brush, rect.Size)) - using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + var rc = rect.ToDirect2D(); + var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon; + + if (brush != null) { - if (brush.PlatformBrush != null) + using (var b = CreateBrush(brush, rect.Size)) { - if (cornerRadius == 0) + if (b.PlatformBrush != null) { - _deviceContext.DrawRectangle( - rect.ToDirect2D(), - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.FillRoundedRectangle( + new RoundedRectangle + { + Rect = new RawRectangleF( + (float)rect.X, + (float)rect.Y, + (float)rect.Right, + (float)rect.Bottom), + RadiusX = (float)radiusX, + RadiusY = (float)radiusY + }, + b.PlatformBrush); + } + else + { + _deviceContext.FillRectangle(rc, b.PlatformBrush); + } } - else + } + } + + if (pen?.Brush != null) + { + using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + { + if (wrapper.PlatformBrush != null) { - _deviceContext.DrawRoundedRectangle( - new RoundedRectangle { Rect = rect.ToDirect2D(), RadiusX = cornerRadius, RadiusY = cornerRadius }, - brush.PlatformBrush, - (float)pen.Thickness, - d2dStroke); + if (isRounded) + { + _deviceContext.DrawRoundedRectangle( + new RoundedRectangle { Rect = rc, RadiusX = (float)radiusX, RadiusY = (float)radiusY }, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } + else + { + _deviceContext.DrawRectangle( + rc, + wrapper.PlatformBrush, + (float)pen.Thickness, + d2dStroke); + } } } } @@ -287,37 +317,18 @@ namespace Avalonia.Direct2D1.Media } /// - /// Draws a filled rectangle. + /// Draws a glyph run. /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius) + /// The foreground. + /// The glyph run. + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var b = CreateBrush(brush, rect.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) { - if (b.PlatformBrush != null) - { - if (cornerRadius == 0) - { - _deviceContext.FillRectangle(rect.ToDirect2D(), b.PlatformBrush); - } - else - { - _deviceContext.FillRoundedRectangle( - new RoundedRectangle - { - Rect = new RawRectangleF( - (float)rect.X, - (float)rect.Y, - (float)rect.Right, - (float)rect.Bottom), - RadiusX = cornerRadius, - RadiusY = cornerRadius - }, - b.PlatformBrush); - } - } + var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + + _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 94de397652..31604ad15f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - public FontManagerImpl() + public string GetDefaultFontFamilyName() { //ToDo: Implement a real lookup of the system's default font. - DefaultFontFamilyName = "segoe ui"; + return "Segoe UI"; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - //ToDo: Implement caching. - return new Typeface(fontFamily, fontWeight, fontStyle); - } - - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) - { - var fontFamilyName = FontFamily.Default.Name; - var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; for (var i = 0; i < familyCount; i++) @@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media continue; } - fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - break; + fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + + return true; } - return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return new GlyphTypefaceImpl(typeface); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b1a177ad24..8e492a66ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -22,10 +22,16 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) - using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, - typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, - (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) + var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; + var familyName = font.FontFamily.FamilyNames.GetString(0); + using (var textFormat = new DWrite.TextFormat( + Direct2D1Platform.DirectWriteFactory, + familyName, + font.FontFamily.FontCollection, + (DWrite.FontWeight)typeface.Weight, + (DWrite.FontStyle)typeface.Style, + DWrite.FontStretch.Normal, + (float)fontSize)) { textFormat.WordWrapping = wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs new file mode 100644 index 0000000000..0b06d5ef3e --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -0,0 +1,19 @@ +using Avalonia.Platform; + +namespace Avalonia.Direct2D1.Media +{ + internal class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) + { + GlyphRun = glyphRun; + } + + public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } + + public void Dispose() + { + GlyphRun?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index 32def01c39..dfc3b48eaa 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media { DWFont = Direct2D1FontCollectionCache.GetFont(typeface); - FontFace = new FontFace(DWFont); + FontFace = new FontFace(DWFont).QueryInterface(); Face = new Face(GetTable); @@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media { StrikethroughThickness = strikethroughThickness; } + + IsFixedPitch = FontFace.IsMonospacedFont; } private Blob GetTable(Face face, Tag tag) @@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media public SharpDX.DirectWrite.Font DWFont { get; } - public FontFace FontFace { get; } + public FontFace1 FontFace { get; } public Face Face { get; } @@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index fbc6d21cb7..6632e2b3e7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -107,7 +107,7 @@ namespace Avalonia.Direct2D1.Media context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); + context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect, _bitmapInterpolationMode); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 1ee869ecb9..29c9280af6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.Media _direct2DBitmap = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); } - public override Vector Dpi => _direct2DBitmap.DotsPerInch.ToAvaloniaVector(); + public override Vector Dpi => new Vector(96, 96); public override PixelSize PixelSize => _direct2DBitmap.PixelSize.ToAvalonia(); public override void Dispose() @@ -58,3 +58,4 @@ namespace Avalonia.Direct2D1.Media } } } +; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 8ec368c999..b96441e357 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -58,7 +58,7 @@ namespace Avalonia.Direct2D1.Media.Imaging { using (var dc = wic.CreateDrawingContext(null)) { - dc.DrawImage( + dc.DrawBitmap( RefCountable.CreateUnownedNotClonable(this), 1, new Rect(PixelSize.ToSizeWithDpi(Dpi.X)), diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 176c3e0e23..4e0853c20c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -26,6 +26,7 @@ namespace Avalonia.Direct2D1.Media using (BitmapDecoder decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); + Dpi = new Vector(96, 96); } } @@ -39,6 +40,7 @@ namespace Avalonia.Direct2D1.Media _decoder = new BitmapDecoder(Direct2D1Platform.ImagingFactory, stream, DecodeOptions.CacheOnLoad); WicImpl = new Bitmap(Direct2D1Platform.ImagingFactory, _decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); + Dpi = new Vector(96, 96); } /// @@ -62,6 +64,7 @@ namespace Avalonia.Direct2D1.Media pixelFormat.Value.ToWic(), BitmapCreateCacheOption.CacheOnLoad); WicImpl.SetResolution(dpi.X, dpi.Y); + Dpi = dpi; } public WicBitmapImpl(APixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) @@ -70,6 +73,8 @@ namespace Avalonia.Direct2D1.Media WicImpl.SetResolution(dpi.X, dpi.Y); PixelFormat = format; + Dpi = dpi; + using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { for (var row = 0; row < size.Height; row++) @@ -82,14 +87,7 @@ namespace Avalonia.Direct2D1.Media } } - public override Vector Dpi - { - get - { - WicImpl.GetResolution(out double x, out double y); - return new Vector(x, y); - } - } + public override Vector Dpi { get; } public override PixelSize PixelSize => WicImpl.Size.ToAvalonia(); diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index a09d5c2d1c..26a8526c16 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs @@ -2,9 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Platform; using Avalonia.Direct2D1; [assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), typeof(Direct2DChecker))] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")] + diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index 11c0a6dca9..b38c09c07a 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; -using System.Text; using Avalonia.Input; using Avalonia.Win32.Interop; @@ -211,7 +210,7 @@ namespace Avalonia.Win32.Input { 31, Key.ImeModeChange }, { 32, Key.Space }, { 33, Key.PageUp }, - { 34, Key.Next }, + { 34, Key.PageDown }, { 35, Key.End }, { 36, Key.Home }, { 37, Key.Left }, @@ -364,17 +363,80 @@ namespace Avalonia.Win32.Input { 254, Key.OemClear }, }; - public static Key KeyFromVirtualKey(int virtualKey) + /// + /// Indicates whether the key is an extended key, such as the right-hand ALT and CTRL keys. + /// According to https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown. + /// + private static bool IsExtended(int keyData) { - Key result; - s_keyFromVirtualKey.TryGetValue(virtualKey, out result); + const int extendedMask = 1 << 24; + + return (keyData & extendedMask) != 0; + } + + private static int GetVirtualKey(int virtualKey, int keyData) + { + // Adapted from https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndKeyboardInputProvider.cs. + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_SHIFT) + { + // Bits from 16 to 23 represent scan code. + const int scanCodeMask = 0xFF0000; + + var scanCode = (keyData & scanCodeMask) >> 16; + + virtualKey = (int)UnmanagedMethods.MapVirtualKey((uint)scanCode, (uint)UnmanagedMethods.MapVirtualKeyMapTypes.MAPVK_VSC_TO_VK_EX); + + if (virtualKey == 0) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LSHIFT; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_MENU) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RMENU; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LMENU; + } + } + + if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_CONTROL) + { + bool isRight = IsExtended(keyData); + + if (isRight) + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RCONTROL; + } + else + { + virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LCONTROL; + } + } + + return virtualKey; + } + + public static Key KeyFromVirtualKey(int virtualKey, int keyData) + { + virtualKey = GetVirtualKey(virtualKey, keyData); + + s_keyFromVirtualKey.TryGetValue(virtualKey, out var result); + return result; } public static int VirtualKeyFromKey(Key key) { - int result; - s_virtualKeyFromKey.TryGetValue(key, out result); + s_virtualKeyFromKey.TryGetValue(key, out var result); + return result; } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 2c6425e26c..904e122382 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -241,6 +241,170 @@ namespace Avalonia.Win32.Interop MK_XBUTTON2 = 0x0040 } + public enum VirtualKeyStates : int + { + VK_LBUTTON = 0x01, + VK_RBUTTON = 0x02, + VK_CANCEL = 0x03, + VK_MBUTTON = 0x04, + VK_XBUTTON1 = 0x05, + VK_XBUTTON2 = 0x06, + VK_BACK = 0x08, + VK_TAB = 0x09, + VK_CLEAR = 0x0C, + VK_RETURN = 0x0D, + VK_SHIFT = 0x10, + VK_CONTROL = 0x11, + VK_MENU = 0x12, + VK_PAUSE = 0x13, + VK_CAPITAL = 0x14, + VK_KANA = 0x15, + VK_HANGEUL = 0x15, + VK_HANGUL = 0x15, + VK_JUNJA = 0x17, + VK_FINAL = 0x18, + VK_HANJA = 0x19, + VK_KANJI = 0x19, + VK_ESCAPE = 0x1B, + VK_CONVERT = 0x1C, + VK_NONCONVERT = 0x1D, + VK_ACCEPT = 0x1E, + VK_MODECHANGE = 0x1F, + VK_SPACE = 0x20, + VK_PRIOR = 0x21, + VK_NEXT = 0x22, + VK_END = 0x23, + VK_HOME = 0x24, + VK_LEFT = 0x25, + VK_UP = 0x26, + VK_RIGHT = 0x27, + VK_DOWN = 0x28, + VK_SELECT = 0x29, + VK_PRINT = 0x2A, + VK_EXECUTE = 0x2B, + VK_SNAPSHOT = 0x2C, + VK_INSERT = 0x2D, + VK_DELETE = 0x2E, + VK_HELP = 0x2F, + VK_LWIN = 0x5B, + VK_RWIN = 0x5C, + VK_APPS = 0x5D, + VK_SLEEP = 0x5F, + VK_NUMPAD0 = 0x60, + VK_NUMPAD1 = 0x61, + VK_NUMPAD2 = 0x62, + VK_NUMPAD3 = 0x63, + VK_NUMPAD4 = 0x64, + VK_NUMPAD5 = 0x65, + VK_NUMPAD6 = 0x66, + VK_NUMPAD7 = 0x67, + VK_NUMPAD8 = 0x68, + VK_NUMPAD9 = 0x69, + VK_MULTIPLY = 0x6A, + VK_ADD = 0x6B, + VK_SEPARATOR = 0x6C, + VK_SUBTRACT = 0x6D, + VK_DECIMAL = 0x6E, + VK_DIVIDE = 0x6F, + VK_F1 = 0x70, + VK_F2 = 0x71, + VK_F3 = 0x72, + VK_F4 = 0x73, + VK_F5 = 0x74, + VK_F6 = 0x75, + VK_F7 = 0x76, + VK_F8 = 0x77, + VK_F9 = 0x78, + VK_F10 = 0x79, + VK_F11 = 0x7A, + VK_F12 = 0x7B, + VK_F13 = 0x7C, + VK_F14 = 0x7D, + VK_F15 = 0x7E, + VK_F16 = 0x7F, + VK_F17 = 0x80, + VK_F18 = 0x81, + VK_F19 = 0x82, + VK_F20 = 0x83, + VK_F21 = 0x84, + VK_F22 = 0x85, + VK_F23 = 0x86, + VK_F24 = 0x87, + VK_NUMLOCK = 0x90, + VK_SCROLL = 0x91, + VK_OEM_NEC_EQUAL = 0x92, + VK_OEM_FJ_JISHO = 0x92, + VK_OEM_FJ_MASSHOU = 0x93, + VK_OEM_FJ_TOUROKU = 0x94, + VK_OEM_FJ_LOYA = 0x95, + VK_OEM_FJ_ROYA = 0x96, + VK_LSHIFT = 0xA0, + VK_RSHIFT = 0xA1, + VK_LCONTROL = 0xA2, + VK_RCONTROL = 0xA3, + VK_LMENU = 0xA4, + VK_RMENU = 0xA5, + VK_BROWSER_BACK = 0xA6, + VK_BROWSER_FORWARD = 0xA7, + VK_BROWSER_REFRESH = 0xA8, + VK_BROWSER_STOP = 0xA9, + VK_BROWSER_SEARCH = 0xAA, + VK_BROWSER_FAVORITES = 0xAB, + VK_BROWSER_HOME = 0xAC, + VK_VOLUME_MUTE = 0xAD, + VK_VOLUME_DOWN = 0xAE, + VK_VOLUME_UP = 0xAF, + VK_MEDIA_NEXT_TRACK = 0xB0, + VK_MEDIA_PREV_TRACK = 0xB1, + VK_MEDIA_STOP = 0xB2, + VK_MEDIA_PLAY_PAUSE = 0xB3, + VK_LAUNCH_MAIL = 0xB4, + VK_LAUNCH_MEDIA_SELECT = 0xB5, + VK_LAUNCH_APP1 = 0xB6, + VK_LAUNCH_APP2 = 0xB7, + VK_OEM_1 = 0xBA, + VK_OEM_PLUS = 0xBB, + VK_OEM_COMMA = 0xBC, + VK_OEM_MINUS = 0xBD, + VK_OEM_PERIOD = 0xBE, + VK_OEM_2 = 0xBF, + VK_OEM_3 = 0xC0, + VK_OEM_4 = 0xDB, + VK_OEM_5 = 0xDC, + VK_OEM_6 = 0xDD, + VK_OEM_7 = 0xDE, + VK_OEM_8 = 0xDF, + VK_OEM_AX = 0xE1, + VK_OEM_102 = 0xE2, + VK_ICO_HELP = 0xE3, + VK_ICO_00 = 0xE4, + VK_PROCESSKEY = 0xE5, + VK_ICO_CLEAR = 0xE6, + VK_PACKET = 0xE7, + VK_OEM_RESET = 0xE9, + VK_OEM_JUMP = 0xEA, + VK_OEM_PA1 = 0xEB, + VK_OEM_PA2 = 0xEC, + VK_OEM_PA3 = 0xED, + VK_OEM_WSCTRL = 0xEE, + VK_OEM_CUSEL = 0xEF, + VK_OEM_ATTN = 0xF0, + VK_OEM_FINISH = 0xF1, + VK_OEM_COPY = 0xF2, + VK_OEM_AUTO = 0xF3, + VK_OEM_ENLW = 0xF4, + VK_OEM_BACKTAB = 0xF5, + VK_ATTN = 0xF6, + VK_CRSEL = 0xF7, + VK_EXSEL = 0xF8, + VK_EREOF = 0xF9, + VK_PLAY = 0xFA, + VK_ZOOM = 0xFB, + VK_NONAME = 0xFC, + VK_PA1 = 0xFD, + VK_OEM_CLEAR = 0xFE + } + public enum WindowActivate { WA_INACTIVE, @@ -581,6 +745,14 @@ namespace Avalonia.Win32.Interop WM_DISPATCH_WORK_ITEM = WM_USER, } + public enum MapVirtualKeyMapTypes : uint + { + MAPVK_VK_TO_VSC = 0x00, + MAPVK_VSC_TO_VK = 0x01, + MAPVK_VK_TO_CHAR = 0x02, + MAPVK_VSC_TO_VK_EX = 0x03, + } + public enum BitmapCompressionMode : uint { BI_RGB = 0, @@ -756,6 +928,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool GetKeyboardState(byte[] lpKeyState); + [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] + public static extern uint MapVirtualKey(uint uCode, uint uMapType); + [DllImport("user32.dll", EntryPoint = "GetMessageW")] public static extern sbyte GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); @@ -1098,7 +1273,10 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); - + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RevokeDragDrop(IntPtr hwnd); + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] public static extern HRESULT OleInitialize(IntPtr val); diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index d454c797fa..c6e04a29b4 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -7,9 +7,9 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class OleContext + internal class OleContext { - private static OleContext fCurrent; + private static OleContext s_current; internal static OleContext Current { @@ -18,13 +18,12 @@ namespace Avalonia.Win32 if (!IsValidOleThread()) return null; - if (fCurrent == null) - fCurrent = new OleContext(); - return fCurrent; + if (s_current == null) + s_current = new OleContext(); + return s_current; } } - private OleContext() { UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero); @@ -43,9 +42,21 @@ namespace Avalonia.Win32 internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) { if (hwnd?.HandleDescriptor != "HWND" || target == null) + { return false; + } return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; } + + internal bool UnregisterDragDrop(IPlatformHandle hwnd) + { + if (hwnd?.HandleDescriptor != "HWND") + { + return false; + } + + return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK; + } } } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index b17e0d6c09..37d047689c 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -6,7 +6,7 @@ using IDataObject = Avalonia.Input.IDataObject; namespace Avalonia.Win32 { - class OleDropTarget : IDropTarget + internal class OleDropTarget : IDropTarget { private readonly IInputRoot _target; private readonly ITopLevelImpl _tl; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9d35342fb9..c16b76b539 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -253,6 +253,11 @@ namespace Avalonia.Win32 public void Dispose() { + if (_dropTarget != null) + { + OleContext.Current?.UnregisterDragDrop(Handle); + _dropTarget = null; + } if (_hwnd != IntPtr.Zero) { UnmanagedMethods.DestroyWindow(_hwnd); @@ -511,7 +516,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_MENUCHAR: @@ -525,7 +530,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), WindowsKeyboardDevice.Instance.Modifiers); break; case UnmanagedMethods.WindowsMessage.WM_CHAR: // Ignore control chars @@ -540,34 +545,42 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: - if(ShouldIgnoreTouchEmulatedMessage()) + case UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN: + if (ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN - ? RawPointerEventType.LeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, + UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, + UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + UnmanagedMethods.WindowsMessage.WM_XBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down + }, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP: case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP: - if(ShouldIgnoreTouchEmulatedMessage()) + case UnmanagedMethods.WindowsMessage.WM_XBUTTONUP: + if (ShouldIgnoreTouchEmulatedMessage()) break; e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP - ? RawPointerEventType.LeftButtonUp - : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP - ? RawPointerEventType.RightButtonUp - : RawPointerEventType.MiddleButtonUp, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, + UnmanagedMethods.WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, + UnmanagedMethods.WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, + UnmanagedMethods.WindowsMessage.WM_XBUTTONUP => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Up : RawPointerEventType.XButton2Up, + }, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; @@ -627,15 +640,19 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: + case UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN: e = new RawPointerEventArgs( _mouseDevice, timestamp, _owner, - msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN - ? RawPointerEventType.NonClientLeftButtonDown - : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN - ? RawPointerEventType.RightButtonDown - : RawPointerEventType.MiddleButtonDown, + (UnmanagedMethods.WindowsMessage)msg switch + { + UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.NonClientLeftButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + UnmanagedMethods.WindowsMessage.WM_NCXBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down, + }, PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; case WindowsMessage.WM_TOUCH: @@ -778,6 +795,10 @@ namespace Avalonia.Win32 modifiers |= RawInputModifiers.RightMouseButton; if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) modifiers |= RawInputModifiers.MiddleMouseButton; + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON1)) + modifiers |= RawInputModifiers.XButton1MouseButton; + if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON2)) + modifiers |= RawInputModifiers.XButton2MouseButton; return modifiers; } @@ -1056,5 +1077,7 @@ namespace Avalonia.Win32 } } IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + + private static int HighWord(int param) => param >> 16; } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 8d8dbb03a2..3efb926ac3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -20,6 +20,16 @@ namespace Avalonia.Base.UnitTests Assert.Equal(100, target.Foo); } + [Fact] + public void Coerces_Set_Value_Attached() + { + var target = new Class1(); + + target.SetValue(Class1.AttachedProperty, 150); + + Assert.Equal(100, target.GetValue(Class1.AttachedProperty)); + } + [Fact] public void Coerces_Bound_Value() { @@ -98,6 +108,12 @@ namespace Avalonia.Base.UnitTests defaultValue: 11, coerce: CoerceFoo); + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + coerce: CoerceFoo); + public int Foo { get => GetValue(FooProperty); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 70de7b449f..391b379c51 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Subjects; +using Avalonia.Controls; using Xunit; namespace Avalonia.Base.UnitTests @@ -34,6 +35,14 @@ namespace Avalonia.Base.UnitTests Assert.Throws(() => target.SetValue(Class1.FooProperty, 101)); } + [Fact] + public void SetValue_Throws_If_Fails_Validation_Attached() + { + var target = new Class1(); + + Assert.Throws(() => target.SetValue(Class1.AttachedProperty, 101)); + } + [Fact] public void Reverts_To_DefaultValue_If_Binding_Fails_Validation() { @@ -69,6 +78,12 @@ namespace Avalonia.Base.UnitTests defaultValue: 11, validate: ValidateFoo); + public static readonly AttachedProperty AttachedProperty = + AvaloniaProperty.RegisterAttached( + "Attached", + defaultValue: 11, + validate: ValidateFoo); + public static bool ValidateFoo(int value) { return value < 100; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index d51f56f558..8eb8d1b5db 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); target.OnNext("foo"); @@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Null_Value_Should_Use_TargetNullValue() + { + var data = new Class1 { StringValue = "foo" }; + + var target = new BindingExpression( + ExpressionObserver.Create(data, o => o.StringValue), + typeof(string), + AvaloniaProperty.UnsetValue, + "bar", + DefaultValueConverter.Instance); + + object result = null; + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + data.StringValue = null; + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; diff --git a/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs new file mode 100644 index 0000000000..a320f07063 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs @@ -0,0 +1,59 @@ +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class BindingsBenchmark + { + [Benchmark] + public void TwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + } + + [Benchmark] + public void UpdateTwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + for (int i = 0; i < 60; i++) + { + instance.IntValue = i; + } + } + private class TestClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + + public static readonly StyledProperty BoundValueProperty = + AvaloniaProperty.Register(nameof(BoundValue)); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int BoundValue + { + get => GetValue(BoundValueProperty); + set => SetValue(BoundValueProperty, value); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs index 7167eafc87..9a756aaf0b 100644 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -21,6 +21,8 @@ namespace Avalonia.Benchmarks public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + public IVisual HitTestFirst(Point p, IVisual root, Func filter) => null; + public void Paint(Rect rect) { } diff --git a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs new file mode 100644 index 0000000000..0ac96c1103 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class SelectorBenchmark + { + private readonly Control _notMatchingControl; + private readonly Calendar _matchingControl; + private readonly Selector _isCalendarSelector; + private readonly Selector _classSelector; + + public SelectorBenchmark() + { + _notMatchingControl = new Control(); + _matchingControl = new Calendar(); + + const string className = "selector-class"; + + _matchingControl.Classes.Add(className); + + _isCalendarSelector = Selectors.Is(null); + _classSelector = Selectors.Class(null, className); + } + + [Benchmark] + public SelectorMatch IsSelector_NoMatch() + { + return _isCalendarSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch IsSelector_Match() + { + return _isCalendarSelector.Match(_matchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_NoMatch() + { + return _classSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_Match() + { + return _classSelector.Match(_matchingControl); + } + } +} diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs new file mode 100644 index 0000000000..fc2380d670 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Traversal +{ + [MemoryDiagnoser] + public class VisualTreeTraversal + { + private readonly TestRoot _root; + private readonly List _controls = new List(); + private readonly List _shuffledControls; + + public VisualTreeTraversal() + { + var panel = new StackPanel(); + _root = new TestRoot { Child = panel, Renderer = new NullRenderer()}; + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4); + + var random = new Random(1); + + _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); + } + + [Benchmark] + public void FindAncestorOfType_Linq() + { + foreach (Control control in _controls) + { + control.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + } + } + + [Benchmark] + public void FindAncestorOfType_Optimized() + { + foreach (Control control in _controls) + { + control.FindAncestorOfType(); + } + } + + [Benchmark] + public void FindCommonVisualAncestor() + { + foreach (IVisual first in _controls) + { + foreach (Control second in _shuffledControls) + { + first.FindCommonVisualAncestor(second); + } + } + } + } +} diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj new file mode 100644 index 0000000000..7fec6f2770 --- /dev/null +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0;net47 + latest + Library + true + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs new file mode 100644 index 0000000000..a1a734f650 --- /dev/null +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using Avalonia.Collections; +using Xunit; + +namespace Avalonia.Controls.DataGrid.UnitTests.Collections +{ + + public class DataGridSortDescriptionTests + { + [Fact] + public void OrderBy_Orders_Correctly_When_Ascending() + { + var items = new[] + { + new Item("b", "b"), + new Item("a", "a"), + new Item("c", "c"), + }; + var expectedResult = items.OrderBy(i => i.Prop1).ToList(); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: false); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.OrderBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void OrderBy_Orders_Correctly_When_Descending() + { + var items = new[] + { + new Item("b", "b"), + new Item("a", "a"), + new Item("c", "c"), + }; + var expectedResult = items.OrderByDescending(i => i.Prop1).ToList(); + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop1), @descending: true); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.OrderBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void ThenBy_Orders_Correctly_When_Ascending() + { + // Casting nonsense below because IOrderedEnumerable isn't covariant in full framework and we need an + // object of type IOrderedEnumerable for DataGridSortDescription.ThenBy + var items = new[] + { + (object)new Item("a", "b"), + new Item("a", "a"), + new Item("a", "c"), + }.OrderBy(i => ((Item)i).Prop1); + var expectedResult = new[] + { + new Item("a", "a"), + new Item("a", "b"), + new Item("a", "c"), + }; + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: false); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.ThenBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + [Fact] + public void ThenBy_Orders_Correctly_When_Descending() + { + // Casting nonsense below because IOrderedEnumerable isn't covariant in full framework and we need an + // object of type IOrderedEnumerable for DataGridSortDescription.ThenBy + var items = new[] + { + (object)new Item("a", "b"), + new Item("a", "a"), + new Item("a", "c"), + }.OrderBy(i => ((Item)i).Prop1); + var expectedResult = new[] + { + new Item("a", "c"), + new Item("a", "b"), + new Item("a", "a"), + }; + var sortDescription = DataGridSortDescription.FromPath(nameof(Item.Prop2), @descending: true); + + sortDescription.Initialize(typeof(Item)); + var result = sortDescription.ThenBy(items).ToList(); + + Assert.Equal(expectedResult, result); + } + + private class Item : IEquatable + { + public Item(string prop1, string prop2) + { + Prop1 = prop1; + Prop2 = prop2; + } + + public string Prop1 { get; } + public string Prop2 { get; } + + public bool Equals(Item other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Prop1 == other.Prop1 && Prop2 == other.Prop2; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Item) obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((Prop1 != null ? Prop1.GetHashCode() : 0) * 397) ^ (Prop2 != null ? Prop2.GetHashCode() : 0); + } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index 71d0d1e328..d3c0e29eca 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -39,7 +39,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Measure_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -62,10 +62,59 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + [Fact] + public void Measure_Should_Return_Correct_Size_With_StretchDirection_DownOnly() + { + var bitmap = CreateBitmap(50, 100); + var target = new Image(); + target.StretchDirection = StretchDirection.DownOnly; + target.Source = bitmap; + + target.Measure(new Size(150, 150)); + + Assert.Equal(new Size(50, 100), target.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(200, double.PositiveInfinity)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, 400)); + + Assert.Equal(new Size(200, 400), image.DesiredSize); + } + + [Fact] + public void Measure_Should_Return_Correct_Size_For_Infinite_Width_Height() + { + var bitmap = CreateBitmap(50, 100); + var image = new Image(); + image.Source = bitmap; + + image.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + Assert.Equal(new Size(50, 100), image.DesiredSize); + } + [Fact] public void Arrange_Should_Return_Correct_Size_For_No_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.None; target.Source = bitmap; @@ -79,7 +128,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Fill; target.Source = bitmap; @@ -93,7 +142,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.Uniform; target.Source = bitmap; @@ -107,7 +156,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() { - var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var bitmap = CreateBitmap(50, 100); var target = new Image(); target.Stretch = Stretch.UniformToFill; target.Source = bitmap; @@ -117,5 +166,10 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(25, 100), target.Bounds.Size); } + + private IBitmap CreateBitmap(int width, int height) + { + return Mock.Of(x => x.Size == new Size(width, height)); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index df2508a3ed..6e87a90ea4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; @@ -245,6 +246,23 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(false, item.IsSelected); } + [Fact] + public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection() + { + var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) + }; + + Prepare(target); + target.Scroll.Offset = new Vector(0, 1); + + items.RemoveRange(0, 11); + } + private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) { _mouse.Click(listBox, item, mouseButton); @@ -383,14 +401,26 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ScrollViewerTemplate() { return new FuncControlTemplate((parent, scope) => - new ScrollContentPresenter + new Panel { - Name = "PART_ContentPresenter", - [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), - [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], - [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], - [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - }.RegisterInNameScope(scope)); + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty], + } + } + }); } private void Prepare(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 989bd744a6..97b6bf9d24 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -199,6 +199,18 @@ namespace Avalonia.Controls.UnitTests.Platform menu.VerifySet(x => x.SelectedItem = null, Times.Never); Assert.False(e.Handled); } + + [Fact] + public void Doesnt_Throw_On_Menu_Keypress() + { + // Issue #3459 + var target = new DefaultMenuInteractionHandler(false); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Tab, Source = menu }; + + target.KeyDown(menu, e); + } } public class NonTopLevel diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 59f3ae44c2..f7942a017d 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -60,7 +60,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); parentMock.As(); - parentMock.As(); + parentMock.As(); (target as ISetLogicalParent).SetParent(parentMock.Object); @@ -105,7 +105,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); - parentMock.As(); + parentMock.As(); parentMock.As().SetupGet(l => l.IsAttachedToLogicalTree).Returns(true); (contentControl as ISetLogicalParent).SetParent(parentMock.Object); @@ -150,7 +150,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var parentMock = new Mock(); parentMock.As(); parentMock.As(); - parentMock.As(); + parentMock.As(); (target as ISetLogicalParent).SetParent(parentMock.Object); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 7a6cf0fba7..308ab4b2b5 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -13,6 +13,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; @@ -1062,7 +1063,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, IStyleRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot, ILogicalRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index f75f6fcf91..7db96a8db7 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -194,7 +194,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private class TemplatedControlWithPopup : TemplatedControl { - public static readonly AvaloniaProperty PopupContentProperty = + public static readonly StyledProperty PopupContentProperty = AvaloniaProperty.Register(nameof(PopupContent)); public TemplatedControlWithPopup() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index e2eb628512..34e6b228d0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(50, target.Value); } - [Fact] - public void Properties_Should_Not_Accept_Nan_And_Inifinity() - { - var target = new TestRange(); - - Assert.Throws(() => target.Minimum = double.NaN); - Assert.Throws(() => target.Minimum = double.PositiveInfinity); - Assert.Throws(() => target.Minimum = double.NegativeInfinity); - Assert.Throws(() => target.Maximum = double.NaN); - Assert.Throws(() => target.Maximum = double.PositiveInfinity); - Assert.Throws(() => target.Maximum = double.NegativeInfinity); - Assert.Throws(() => target.Value = double.NaN); - Assert.Throws(() => target.Value = double.PositiveInfinity); - Assert.Throws(() => target.Value = double.NegativeInfinity); - } - [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index d819581000..696c0dbf46 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void SelectedIndex_Should_Be_Minus_1_Without_Initialize() + { + var items = new[] + { + new Item(), + new Item(), + }; + + var target = new ListBox(); + target.Items = items; + target.Template = Template(); + target.DataContext = new object(); + + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs index 4f4ab47b0a..9acd42aba6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -1,5 +1,4 @@ using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests Assert.Null(threeStateButton.IsChecked); } + [Fact] + public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes() + { + var threeStateButton = new ToggleButton(); + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.IsChecked = true; + Assert.True(checkedRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.IsChecked = false; + Assert.True(uncheckedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.IsChecked = null; + Assert.True(indeterminateRaised); + } + + [Fact] + public void ToggleButton_Events_Are_Raised_When_Toggling() + { + var threeStateButton = new TestToggleButton { IsThreeState = true }; + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.Toggle(); + Assert.True(checkedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.Toggle(); + Assert.True(indeterminateRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.Toggle(); + Assert.True(uncheckedRaised); + } + private class Class1 : NotifyingBase { private bool _foo; @@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests set { nullableFoo = value; RaisePropertyChanged(); } } } + + private class TestToggleButton : ToggleButton + { + public new void Toggle() => base.Toggle(); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 72c81659c3..91592b1ff7 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -166,6 +166,31 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Impl_Close_Should_Raise_DetachedFromLogicalTree_Event() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + + var target = new TestTopLevel(impl.Object); + var raised = 0; + + target.DetachedFromLogicalTree += (s, e) => + { + Assert.Same(target, e.Root); + Assert.Same(target, e.Source); + Assert.Null(e.Parent); + ++raised; + }; + + impl.Object.Closed(); + + Assert.Equal(1, raised); + } + } + [Fact] public void Impl_Input_Should_Pass_Input_To_InputManager() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index a91b7a0701..a2892be5c2 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; @@ -33,6 +34,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -77,6 +80,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -527,6 +532,8 @@ namespace Avalonia.Controls.UnitTests Items = data, }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -893,6 +900,37 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, GetItem(target, 0, 1, 0).Level); } + [Fact] + public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() + { + // Issue #2985 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var parent = tree[0]; + var node = parent.Children[1]; + + parent.Children.Remove(node); + parent.Children.Add(node); + + var item = target.ItemContainerGenerator.Index.ContainerFromItem(node); + ApplyTemplates(new[] { item }); + + // #2985 causes ArgumentException here. + node.Children.Add(new Node()); + } + [Fact] public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { @@ -933,6 +971,40 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Removing_TreeView_From_Root_Should_Preserve_TreeViewItems() + { + // Issue #3328 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var root = new TestRoot(); + root.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + + root.Child = null; + + Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count()); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + + var rootNode = Assert.IsType(target.Presenter.Panel.Children[0]); + Assert.Equal(3, rootNode.ItemContainerGenerator.Containers.Count()); + Assert.Equal(3, rootNode.Presenter.Panel.Children.Count); + + var child2Node = Assert.IsType(rootNode.Presenter.Panel.Children[1]); + Assert.Equal(1, child2Node.ItemContainerGenerator.Containers.Count()); + Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index d87014f646..0508edd92f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,8 +228,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var windowImpl = Mock.Of(x => x.Scaling == 1); - var target = new Window(windowImpl); + var target = new Window(); target.Show(); target.Close(); diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs new file mode 100644 index 0000000000..3320bcebca --- /dev/null +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -0,0 +1,135 @@ +using System; +using System.Reflection; +using Avalonia.Direct2D1.Media; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Direct2D1.UnitTests.Media +{ + public class FontManagerImplTests + { + private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"; + + [Fact] + public void Should_Create_Typeface_From_Fallback() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, Arial"))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Create_Typeface_From_Fallback_Bold() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Create_Typeface_For_Unknown_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("Unknown"))); + + var font = glyphTypeface.DWFont; + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Load_Typeface_From_Resource() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + + [Fact] + public void Should_Load_Nearest_Matching_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + } +} diff --git a/tests/Avalonia.Input.UnitTests/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs index 39c219a773..ffa03f9e8e 100644 --- a/tests/Avalonia.Input.UnitTests/GesturesTests.cs +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -135,7 +135,7 @@ namespace Avalonia.Interactivity.UnitTests } [Fact] - public void DoubleTapped_Should_Be_Raised_For_Middle_Button() + public void DoubleTapped_Should_Not_Be_Raised_For_Middle_Button() { Border border = new Border(); var decorator = new Decorator @@ -149,7 +149,7 @@ namespace Avalonia.Interactivity.UnitTests _mouse.Click(border, MouseButton.Middle); _mouse.Down(border, MouseButton.Middle, clickCount: 2); - Assert.True(raised); + Assert.False(raised); } [Fact] diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 214aead521..68d4e3ada9 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -238,6 +238,9 @@ namespace Avalonia.Input.UnitTests { renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(new[] { hit }); + + renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit); } private IDisposable TestApplication(IRenderer renderer) diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index a683e5cfca..f063d59ca4 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests It.IsAny>())) .Returns(new FormattedTextMock("TEST")); + renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl()); + var streamGeometry = new Mock(); streamGeometry.Setup(x => x.Open()) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 1da4746516..389b3c8df8 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -397,6 +397,8 @@ namespace Avalonia.LeakTests public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + public IVisual HitTestFirst(Point p, IVisual root, Func filter) => null; + public void Paint(Rect rect) { } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index e29f4bfa9c..d85f6ceac8 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(42, target.Value); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + var source = new Source { Foo = null }; + + var binding = new Binding + { + Source = source, + Path = "Foo", + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + [Fact] public void Null_Path_Should_Bind_To_DataContext() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 78c538d99d..f3e8ff9248 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("fallback", target.Text); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + + var binding = new MultiBinding + { + Converter = new NullValueConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, + }, + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) @@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data return AvaloniaProperty.UnsetValue; } } + + private class NullValueConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index c3bc649abb..76edf9a17a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -39,6 +39,59 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void BindingExtension_Binds_To_TargetNullValue() + { + using (StyledWindow()) + { + var xaml = @" + + + foobar + + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void BindingExtension_TargetNullValue_UnsetByDefault() + { + using (StyledWindow()) + { + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new FooBar(); + window.Show(); + + Assert.Equal(false, textBlock.IsVisible); + } + } + + private class FooBar + { + public object Foo { get; } = null; + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f4d4a9dd2a..eaf9f22406 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -307,6 +307,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("child", child.Name); } + [Fact] + public void ControlTemplate_With_TargetType_Is_Operational() + { + var xaml = @" + + + +"; + var template = AvaloniaXamlLoader.Parse(xaml); + + Assert.Equal(typeof(ContentControl), template.TargetType); + + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); + } + [Fact] public void ControlTemplate_With_Panel_Children_Are_Added() { @@ -704,11 +721,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - - [Fact(Skip = -@"Doesn't work with Portable.xaml, it's working in different creation order - -Handled in test 'Control_Is_Added_To_Parent_Before_Final_EndInit' -do we need it?")] + [Fact] public void Control_Is_Added_To_Parent_Before_Properties_Are_Set() { using (UnitTestApplication.Start(TestServices.StyledWindow)) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs new file mode 100644 index 0000000000..d0cdef3c0b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class ResourceDictionaryTests : XamlTestBase + { + [Fact] + public void StaticResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Works_In_ResourceDictionary() + { + using (StyledWindow()) + { + var xaml = @" + + Red + +"; + var loader = new AvaloniaXamlLoader(); + var resources = (ResourceDictionary)loader.Load(xaml); + var brush = (SolidColorBrush)resources["RedBrush"]; + + Assert.Equal(Colors.Red, brush.Color); + } + } + + [Fact] + public void DynamicResource_Finds_Resource_In_Parent_Dictionary() + { + var dictionaryXaml = @" + + +"; + + using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml))) + { + var xaml = @" + + + + + + + + Red + +