Browse Source

Merge branch 'master' into refactor/use-selectionmodel

Dariusz Komosiński 5 years ago
parent
commit
3910c7c856
73 changed files with 1971 additions and 532 deletions
  1. 5 0
      .ncrunch/BindingDemo.v3.ncrunchproject
  2. 5 0
      .ncrunch/RenderDemo.v3.ncrunchproject
  3. 5 0
      .ncrunch/VirtualizationDemo.v3.ncrunchproject
  4. 4 4
      azure-pipelines.yml
  5. 7 1
      native/Avalonia.Native/inc/avalonia-native.h
  6. 62 22
      native/Avalonia.Native/src/OSX/window.mm
  7. 14 21
      readme.md
  8. 9 2
      samples/ControlCatalog/MainView.xaml
  9. 14 0
      samples/ControlCatalog/MainView.xaml.cs
  10. 4 2
      samples/ControlCatalog/MainWindow.xaml
  11. 9 5
      samples/ControlCatalog/MainWindow.xaml.cs
  12. 18 1
      samples/ControlCatalog/Pages/ImagePage.xaml
  13. 33 0
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  14. 2 2
      samples/ControlCatalog/Pages/MenuPage.xaml
  15. 5 1
      src/Avalonia.Base/AvaloniaObject.cs
  16. 47 41
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  17. 25 1
      src/Avalonia.Controls/Application.cs
  18. 1 1
      src/Avalonia.Controls/Calendar/DatePicker.cs
  19. 2 1
      src/Avalonia.Controls/ComboBox.cs
  20. 195 0
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  21. 47 0
      src/Avalonia.Controls/MenuItem.cs
  22. 1 1
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  23. 0 11
      src/Avalonia.Controls/Primitives/AccessText.cs
  24. 1 1
      src/Avalonia.Controls/Primitives/Popup.cs
  25. 45 15
      src/Avalonia.Controls/TextBlock.cs
  26. 37 4
      src/Avalonia.Controls/TextBox.cs
  27. 16 4
      src/Avalonia.Controls/TopLevel.cs
  28. 82 14
      src/Avalonia.Controls/Window.cs
  29. 1 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  30. 1 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  31. 1 1
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  32. 36 8
      src/Avalonia.Input/KeyGesture.cs
  33. 3 3
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  34. 200 0
      src/Avalonia.Interactivity/EventRoute.cs
  35. 0 20
      src/Avalonia.Interactivity/EventSubscription.cs
  36. 10 3
      src/Avalonia.Interactivity/IInteractive.cs
  37. 96 197
      src/Avalonia.Interactivity/Interactive.cs
  38. 27 3
      src/Avalonia.Interactivity/InteractiveExtensions.cs
  39. 12 8
      src/Avalonia.Interactivity/RoutedEvent.cs
  40. 4 4
      src/Avalonia.Interactivity/RoutedEventArgs.cs
  41. 3 3
      src/Avalonia.Interactivity/RoutedEventRegistry.cs
  42. 7 0
      src/Avalonia.Layout/Layoutable.cs
  43. 2 2
      src/Avalonia.Native/WindowImpl.cs
  44. 128 11
      src/Avalonia.Styling/StyledElement.cs
  45. 6 3
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  46. 14 0
      src/Avalonia.Styling/Styling/IGlobalStyles.cs
  47. 7 1
      src/Avalonia.Styling/Styling/IStyle.cs
  48. 16 0
      src/Avalonia.Styling/Styling/IStyleHost.cs
  49. 11 0
      src/Avalonia.Styling/Styling/IStyleable.cs
  50. 2 0
      src/Avalonia.Styling/Styling/Style.cs
  51. 104 39
      src/Avalonia.Styling/Styling/Styles.cs
  52. 27 8
      src/Avalonia.Themes.Default/MenuItem.xaml
  53. 1 0
      src/Avalonia.Themes.Default/NativeMenuBar.xaml
  54. 96 0
      src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs
  55. 18 9
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  56. 12 13
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  57. 1 0
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  58. 8 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  59. 11 5
      src/Avalonia.X11/X11Window.cs
  60. 14 9
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  61. 10 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  62. 12 8
      src/Windows/Avalonia.Win32/WindowImpl.cs
  63. 1 1
      src/iOS/Avalonia.iOS/EmbeddableImpl.cs
  64. 11 0
      tests/Avalonia.Benchmarks/NullGlyphRun.cs
  65. 3 1
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  66. 43 0
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  67. 39 0
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  68. 25 6
      tests/Avalonia.Input.UnitTests/KeyGestureTests.cs
  69. 24 1
      tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs
  70. 11 0
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs
  71. 38 5
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  72. 179 0
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  73. 1 1
      tests/Avalonia.UnitTests/MouseTestHelper.cs

+ 5 - 0
.ncrunch/BindingDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/RenderDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/VirtualizationDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 4 - 4
azure-pipelines.yml

@@ -35,16 +35,16 @@ jobs:
     vmImage: 'macOS-10.14'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.x'
+    displayName: 'Use .NET Core SDK 3.1.101'
     inputs:
       packageType: sdk
-      version: 3.1.x
+      version: 3.1.101
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core Runtime 3.1.x'
+    displayName: 'Use .NET Core Runtime 3.1.1'
     inputs:
       packageType: runtime
-      version: 3.1.x
+      version: 3.1.1
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'

+ 7 - 1
native/Avalonia.Native/inc/avalonia-native.h

@@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession;
 struct IAvnAppMenu;
 struct IAvnAppMenuItem;
 
+enum SystemDecorations {
+    SystemDecorationsNone = 0,
+    SystemDecorationsBorderOnly = 1,
+    SystemDecorationsFull = 2,
+};
+
 struct AvnSize
 {
     double Width, Height;
@@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
 {
     virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
     virtual HRESULT SetCanResize(bool value) = 0;
-    virtual HRESULT SetHasDecorations(bool value) = 0;
+    virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
     virtual HRESULT SetTitle (void* utf8Title) = 0;
     virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
     virtual HRESULT SetWindowState(AvnWindowState state) = 0;

+ 62 - 22
native/Avalonia.Native/src/OSX/window.mm

@@ -115,7 +115,6 @@ public:
             [NSApp activateIgnoringOtherApps:YES];
             
             [Window setTitle:_lastTitle];
-            [Window setTitleVisibility:NSWindowTitleVisible];
         
             return S_OK;
         }
@@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub
 {
 private:
     bool _canResize = true;
-    bool _hasDecorations = true;
+    SystemDecorations _hasDecorations = SystemDecorationsFull;
     CGRect _lastUndecoratedFrame;
     AvnWindowState _lastWindowState;
     
@@ -427,6 +426,7 @@ private:
     ComPtr<IAvnWindowEvents> WindowEvents;
     WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
     {
+        _lastWindowState = Normal;
         WindowEvents = events;
         [Window setCanBecomeKeyAndMain];
         [Window disableCursorRects];
@@ -440,7 +440,7 @@ private:
                 [[Window parentWindow] removeChildWindow:Window];
             WindowBaseImpl::Show();
             
-            return SetWindowState(Normal);
+            return SetWindowState(_lastWindowState);
         }
     }
     
@@ -476,23 +476,26 @@ private:
     
     bool IsZoomed ()
     {
-        return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized();
+        return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
     }
     
     void DoZoom()
     {
-        if (_hasDecorations)
+        switch (_hasDecorations)
         {
-            [Window performZoom:Window];
-        }
-        else
-        {
-            if (!UndecoratedIsMaximized())
-            {
-                _lastUndecoratedFrame = [Window frame];
-            }
-            
-            [Window zoom:Window];
+            case SystemDecorationsNone:
+                if (!UndecoratedIsMaximized())
+                {
+                    _lastUndecoratedFrame = [Window frame];
+                }
+                
+                [Window zoom:Window];
+                break;
+
+            case SystemDecorationsBorderOnly:
+            case SystemDecorationsFull:
+                [Window performZoom:Window];
+                break;
         }
     }
     
@@ -506,13 +509,35 @@ private:
         }
     }
     
-    virtual HRESULT SetHasDecorations(bool value) override
+    virtual HRESULT SetHasDecorations(SystemDecorations value) override
     {
         @autoreleasepool
         {
             _hasDecorations = value;
             UpdateStyle();
-            
+
+            switch (_hasDecorations)
+            {
+                case SystemDecorationsNone:
+                    [Window setHasShadow:NO];
+                    [Window setTitleVisibility:NSWindowTitleHidden];
+                    [Window setTitlebarAppearsTransparent:YES];
+                    break;
+
+                case SystemDecorationsBorderOnly:
+                    [Window setHasShadow:YES];
+                    [Window setTitleVisibility:NSWindowTitleHidden];
+                    [Window setTitlebarAppearsTransparent:YES];
+                    break;
+
+                case SystemDecorationsFull:
+                    [Window setHasShadow:YES];
+                    [Window setTitleVisibility:NSWindowTitleVisible];
+                    [Window setTitlebarAppearsTransparent:NO];
+                    [Window setTitle:_lastTitle];
+                    break;
+            }
+
             return S_OK;
         }
     }
@@ -523,7 +548,6 @@ private:
         {
             _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title];
             [Window setTitle:_lastTitle];
-            [Window setTitleVisibility:NSWindowTitleVisible];
             
             return S_OK;
         }
@@ -645,10 +669,26 @@ protected:
     virtual NSWindowStyleMask GetStyle() override
     {
         unsigned long s = NSWindowStyleMaskBorderless;
-        if(_hasDecorations)
-            s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
-        if(_canResize)
-            s = s | NSWindowStyleMaskResizable;
+
+        switch (_hasDecorations)
+        {
+            case SystemDecorationsNone:
+                break;
+
+            case SystemDecorationsBorderOnly:
+                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
+                break;
+
+            case SystemDecorationsFull:
+                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
+                if(_canResize)
+                {
+                    s = s | NSWindowStyleMaskResizable;
+                }
+
+                break;
+        }
+
         return s;
     }
 };

+ 14 - 21
readme.md

@@ -8,25 +8,21 @@
 
 ## About
 
-**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS.
+**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS.
 
-**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
+**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
 
-| Control catalog | Desktop platforms | Mobile platforms |
-|---|---|---|
-| <a href='https://youtu.be/wHcB3sGLVYg'><img width='300' src='http://avaloniaui.net/images/screen.png'></a> | <a href='https://www.youtube.com/watch?t=28&v=c_AB_XSILp0' target='_blank'><img width='300' src='http://avaloniaui.net/images/avalonia-video.png'></a> | <a href='https://www.youtube.com/watch?v=NJ9-hnmUbBM' target='_blank'><img width='300' src='https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg'></a> |
+To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
 
-[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources.
+You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been.
 
-## Getting Started
-
-Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
+[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
 
-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).
+## Getting Started
 
-If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature,  so only **YOU** can make it happen.
+The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
 
-Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
+Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
 
 Use these commands in the Package Manager console to install Avalonia manually:
 ```
@@ -34,18 +30,17 @@ Install-Package Avalonia
 Install-Package Avalonia.Desktop
 ```
 
-## Bleeding Edge Builds
+## JetBrains Rider
 
-or use nightly build feeds as described here:
-https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed
+If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.
 
-## Documentation
+## Bleeding Edge Builds
 
-You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). 
+We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed!
 
-There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
+## Documentation
 
-Contributions for our docs are always welcome!
+Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers.
 
 ## Building and Using
 
@@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con
 This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)].
 <a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
 
-
 ### Backers
 
 Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
 
 <a href="https://opencollective.com/Avalonia#backers" target="_blank"><img src="https://opencollective.com/Avalonia/backers.svg?width=890"></a>
 
-
 ### Sponsors
 
 Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)]

+ 9 - 2
samples/ControlCatalog/MainView.xaml

@@ -59,10 +59,17 @@
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
       <TabControl.Tag>
-        <ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+        <StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+          <ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
+            <ComboBoxItem>No Decorations</ComboBoxItem>
+            <ComboBoxItem>Border Only</ComboBoxItem>
+            <ComboBoxItem>Full Decorations</ComboBoxItem>
+          </ComboBox>
+          <ComboBox x:Name="Themes" SelectedIndex="0">
             <ComboBoxItem>Light</ComboBoxItem>
             <ComboBoxItem>Dark</ComboBoxItem>
-        </ComboBox>
+          </ComboBox>
+        </StackPanel>
       </TabControl.Tag>
     </TabControl>
   </Grid>

+ 14 - 0
samples/ControlCatalog/MainView.xaml.cs

@@ -56,6 +56,20 @@ namespace ControlCatalog
                 }
             };
             Styles.Add(light);
+
+            var decorations = this.Find<ComboBox>("Decorations");
+            decorations.SelectionChanged += (sender, e) =>
+            {
+                Window window = (Window)VisualRoot;
+                window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
+            };
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            var decorations = this.Find<ComboBox>("Decorations");
+            decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations;
         }
     }
 }

+ 4 - 2
samples/ControlCatalog/MainWindow.xaml

@@ -14,7 +14,7 @@
       <NativeMenuItem Header="File">
         <NativeMenuItem.Menu>
           <NativeMenu>
-            <NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
+            <NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
             <NativeMenuItemSeperator/>
             <NativeMenuItem Header="Recent">
               <NativeMenuItem.Menu>
@@ -22,7 +22,9 @@
               </NativeMenuItem.Menu>
             </NativeMenuItem>
             <NativeMenuItemSeperator/>
-            <NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/>
+            <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
+                            Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
+                            Clicked="OnCloseClicked" />
           </NativeMenu>
         </NativeMenuItem.Menu>
       </NativeMenuItem>

+ 9 - 5
samples/ControlCatalog/MainWindow.xaml.cs

@@ -1,13 +1,11 @@
+using System;
+using System.Runtime.InteropServices;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Notifications;
-using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Markup.Xaml;
-using Avalonia.Threading;
 using ControlCatalog.ViewModels;
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
 
 namespace ControlCatalog
 {
@@ -35,6 +33,12 @@ namespace ControlCatalog
             mainMenu.AttachedToVisualTree += MenuAttached;
         }
 
+        public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
+
+        public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
+            new KeyGesture(Key.Q, KeyModifiers.Meta) :
+            new KeyGesture(Key.F4, KeyModifiers.Alt);
+
         public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
         {
             if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)

+ 18 - 1
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -7,7 +7,7 @@
       <TextBlock Classes="h2">Displays an image</TextBlock>
     </StackPanel>
 
-    <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64">
+    <Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,*" Margin="64">
       
       <DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
         <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
@@ -22,6 +22,23 @@
       </DockPanel>
 
       <DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Crop</TextBlock>
+        <ComboBox Name="bitmapCrop" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapCropChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Center</ComboBoxItem>
+          <ComboBoxItem>TopLeft</ComboBoxItem>
+          <ComboBoxItem>TopRight</ComboBoxItem>
+          <ComboBoxItem>BottomLeft</ComboBoxItem>
+          <ComboBoxItem>BottomRight</ComboBoxItem>
+        </ComboBox>
+        <Image Name="croppedImage">
+          <Image.Source>
+            <CroppedBitmap  Source="/Assets/delicate-arch-896885_640.jpg" SourceRect="0 0 320 240"/>  
+          </Image.Source>
+        </Image>
+      </DockPanel>
+
+      <DockPanel Grid.Column="2" Grid.Row="1" Margin="16">
         <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
         <ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
           <ComboBoxItem>None</ComboBoxItem>

+ 33 - 0
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@@ -1,6 +1,10 @@
+using System;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
 
 namespace ControlCatalog.Pages
 {
@@ -8,12 +12,14 @@ namespace ControlCatalog.Pages
     {
         private readonly Image _bitmapImage;
         private readonly Image _drawingImage;
+        private readonly Image _croppedImage;
 
         public ImagePage()
         {
             InitializeComponent();
             _bitmapImage = this.FindControl<Image>("bitmapImage");
             _drawingImage = this.FindControl<Image>("drawingImage");
+            _croppedImage = this.FindControl<Image>("croppedImage");
         }
 
         private void InitializeComponent()
@@ -38,5 +44,32 @@ namespace ControlCatalog.Pages
                 _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
             }
         }
+
+        public void BitmapCropChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (_croppedImage != null)
+            {
+                var comboxBox = (ComboBox)sender;
+                var croppedBitmap = _croppedImage.Source as CroppedBitmap;
+                croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
+            }
+        }
+
+        private PixelRect GetCropRect(int index)
+        {
+            var bitmapWidth = 640;
+            var bitmapHeight = 426;
+            var cropSize = new PixelSize(320, 240);
+            return index switch
+            {
+                1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize),
+                2 => new PixelRect(new PixelPoint(0, 0), cropSize),
+                3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize),
+                4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize),
+                5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize),
+                _ => PixelRect.Empty
+            };
+            
+        }
     }
 }

+ 2 - 2
samples/ControlCatalog/Pages/MenuPage.xaml

@@ -16,13 +16,13 @@
                 <TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
                 <Menu>
                     <MenuItem Header="_First">
-                        <MenuItem Header="Standard _Menu Item"/>
+                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
                         <Separator/>
                         <MenuItem Header="Menu with _Submenu">
                             <MenuItem Header="Submenu _1"/>
                             <MenuItem Header="Submenu _2"/>
                         </MenuItem>
-                        <MenuItem Header="Menu Item with _Icon">
+                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
                             <MenuItem.Icon>
                                 <Image Source="/Assets/github_icon.png"/>
                             </MenuItem.Icon>

+ 5 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -80,8 +80,12 @@ namespace Avalonia
                     _inheritanceParent?.RemoveInheritanceChild(this);
                     _inheritanceParent = value;
 
-                    foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
+                    var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
+                    var propertiesCount = properties.Count;
+
+                    for (var i = 0; i < propertiesCount; i++)
                     {
+                        var property = properties[i];
                         if (valuestore?.IsSet(property) == true)
                         {
                             // If local value set there can be no change.

+ 47 - 41
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -3,9 +3,7 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Runtime.CompilerServices;
-using Avalonia.Data;
 
 namespace Avalonia
 {
@@ -28,8 +26,6 @@ namespace Avalonia
             new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
-        private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
-            new Dictionary<Type, List<PropertyInitializationData>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
 
@@ -49,7 +45,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegistered(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -83,7 +79,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredAttached(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -114,7 +110,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredDirect(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -145,7 +141,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredInherited(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -157,16 +153,27 @@ namespace Avalonia
             result = new List<AvaloniaProperty>();
             var visited = new HashSet<AvaloniaProperty>();
 
-            foreach (var property in GetRegistered(type))
+            var registered = GetRegistered(type);
+            var registeredCount = registered.Count;
+
+            for (var i = 0; i < registeredCount; i++)
             {
+                var property = registered[i];
+
                 if (property.Inherits)
                 {
                     result.Add(property);
                     visited.Add(property);
                 }
             }
-            foreach (var property in GetRegisteredAttached(type))
+
+            var registeredAttached = GetRegisteredAttached(type);
+            var registeredAttachedCount = registeredAttached.Count;
+
+            for (var i = 0; i < registeredAttachedCount; i++)
             {
+                var property = registeredAttached[i];
+
                 if (property.Inherits)
                 {
                     if (!visited.Contains(property))
@@ -185,7 +192,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The object.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
+        public IReadOnlyList<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
         {
             Contract.Requires<ArgumentNullException>(o != null);
 
@@ -229,8 +236,13 @@ namespace Avalonia
                 throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            foreach (AvaloniaProperty x in GetRegistered(type))
+            var registered = GetRegistered(type);
+            var registeredCount = registered.Count;
+
+            for (var i = 0; i < registeredCount; i++)
             {
+                AvaloniaProperty x = registered[i];
+
                 if (x.Name == name)
                 {
                     return x;
@@ -276,8 +288,13 @@ namespace Avalonia
                 return property;
             }
 
-            foreach (var p in GetRegisteredDirect(o.GetType()))
+            var registeredDirect = GetRegisteredDirect(o.GetType());
+            var registeredDirectCount = registeredDirect.Count;
+
+            for (var i = 0; i < registeredDirectCount; i++)
             {
+                var p = registeredDirect[i];
+
                 if (p == property)
                 {
                     return (DirectPropertyBase<T>)p;
@@ -308,8 +325,23 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            return Instance.GetRegistered(type).Any(x => x == property) ||
-                Instance.GetRegisteredAttached(type).Any(x => x == property);
+            static bool ContainsProperty(IReadOnlyList<AvaloniaProperty> properties, AvaloniaProperty property)
+            {
+                var propertiesCount = properties.Count;
+
+                for (var i = 0; i < propertiesCount; i++)
+                {
+                    if (properties[i] == property)
+                    {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+
+            return ContainsProperty(Instance.GetRegistered(type), property) ||
+                   ContainsProperty(Instance.GetRegisteredAttached(type), property);
         }
 
         /// <summary>
@@ -374,7 +406,6 @@ namespace Avalonia
             }
             
             _registeredCache.Clear();
-            _initializedCache.Clear();
             _inheritedCache.Clear();
         }
 
@@ -411,32 +442,7 @@ namespace Avalonia
             }
             
             _attachedCache.Clear();
-            _initializedCache.Clear();
             _inheritedCache.Clear();
         }
-
-        private readonly struct PropertyInitializationData
-        {
-            public AvaloniaProperty Property { get; }
-            public object Value { get; }
-            public bool IsDirect { get; }
-            public IDirectPropertyAccessor DirectAccessor { get; }
-
-            public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor)
-            {
-                Property = property;
-                Value = null;
-                IsDirect = true;
-                DirectAccessor = directAccessor;
-            }
-
-            public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type)
-            {
-                Property = property;
-                Value = styledAccessor.GetDefaultValue(type);
-                IsDirect = false;
-                DirectAccessor = null;
-            }
-        }
     }
 }

+ 25 - 1
src/Avalonia.Controls/Application.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.Collections.Generic;
 using System.Reactive.Concurrency;
 using System.Threading;
 using Avalonia.Animation;
@@ -45,6 +46,8 @@ namespace Avalonia
         private Styles _styles;
         private IResourceDictionary _resources;
         private bool _notifyingResourcesChanged;
+        private Action<IReadOnlyList<IStyle>> _stylesAdded;
+        private Action<IReadOnlyList<IStyle>> _stylesRemoved;
 
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -201,6 +204,18 @@ namespace Avalonia
         /// </summary>
         public IApplicationLifetime ApplicationLifetime { get; set; }
 
+        event Action<IReadOnlyList<IStyle>> IGlobalStyles.GlobalStylesAdded
+        {
+            add => _stylesAdded += value;
+            remove => _stylesAdded -= value;
+        }
+
+        event Action<IReadOnlyList<IStyle>> IGlobalStyles.GlobalStylesRemoved
+        {
+            add => _stylesRemoved += value;
+            remove => _stylesRemoved -= value;
+        }
+
         /// <summary>
         /// Initializes the application by loading XAML etc.
         /// </summary>
@@ -214,6 +229,16 @@ namespace Avalonia
                    Styles.TryGetResource(key, out value);
         }
 
+        void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
+        {
+            _stylesAdded?.Invoke(styles);
+        }
+
+        void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> styles)
+        {
+            _stylesRemoved?.Invoke(styles);
+        }
+
         /// <summary>
         /// Register's the services needed by Avalonia.
         /// </summary>
@@ -286,6 +311,5 @@ namespace Avalonia
             get => _name;
             set => SetAndRaise(NameProperty, ref _name, value);
         }
-
     }
 }

+ 1 - 1
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -476,7 +476,7 @@ namespace Avalonia.Controls
             {
                 _dropDownButton.Click += DropDownButton_Click;
                 _buttonPointerPressedSubscription =
-                    _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
+                    _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
             }
 
             if (_textBox != null)

+ 2 - 1
src/Avalonia.Controls/ComboBox.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.VisualTree;
@@ -265,7 +266,7 @@ namespace Avalonia.Controls
             var toplevel = this.GetVisualRoot() as TopLevel;
             if (toplevel != null)
             {
-                _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
+                _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
                 {
                     //eat wheel scroll event outside dropdown popup while it's open
                     if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)

+ 195 - 0
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@@ -0,0 +1,195 @@
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Data.Converters;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Converters
+{
+    /// <summary>
+    /// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
+    /// platform's style guidelines.
+    /// </summary>
+    public class PlatformKeyGestureConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (value is null)
+            {
+                return null;
+            }
+            else if (value is KeyGesture gesture && targetType == typeof(string))
+            {
+                return ToPlatformString(gesture);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
+        /// platform's style guidelines.
+        /// </summary>
+        /// <param name="gesture">The gesture.</param>
+        /// <returns>The gesture formatted according to the current platform.</returns>
+        public static string ToPlatformString(KeyGesture gesture)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return ToString(gesture, "Win");
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                return ToString(gesture, "Super");
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                return ToOSXString(gesture);
+            }
+            else
+            {
+                return gesture.ToString();
+            }
+        }
+
+        private static string ToString(KeyGesture gesture, string meta)
+        {
+            var s = new StringBuilder();
+
+            static void Plus(StringBuilder s)
+            {
+                if (s.Length > 0)
+                {
+                    s.Append("+");
+                }
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append("Ctrl");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                Plus(s);
+                s.Append("Shift");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                Plus(s);
+                s.Append("Alt");
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                Plus(s);
+                s.Append(meta);
+            }
+
+            Plus(s);
+            s.Append(ToString(gesture.Key));
+
+            return s.ToString();
+        }
+
+        private static string ToOSXString(KeyGesture gesture)
+        {
+            var s = new StringBuilder();
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append('⌃');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                s.Append('⌥');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                s.Append('⇧');
+            }
+
+            if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                s.Append('⌘');
+            }
+
+            s.Append(ToOSXString(gesture.Key));
+
+            return s.ToString();
+        }
+
+        private static string ToString(Key key)
+        {
+            return key switch
+            {
+                Key.Add => "+",
+                Key.Back => "Backspace",
+                Key.D0 => "0",
+                Key.D1 => "1",
+                Key.D2 => "2",
+                Key.D3 => "3",
+                Key.D4 => "4",
+                Key.D5 => "5",
+                Key.D6 => "6",
+                Key.D7 => "7",
+                Key.D8 => "8",
+                Key.D9 => "9",
+                Key.Decimal => ".",
+                Key.Divide => "/",
+                Key.Down => "Down Arrow",
+                Key.Left => "Left Arrow",
+                Key.Multiply => "*",
+                Key.OemBackslash => "\\",
+                Key.OemCloseBrackets => "]",
+                Key.OemComma => ",",
+                Key.OemMinus => "-",
+                Key.OemOpenBrackets => "[",
+                Key.OemPeriod=> ".",
+                Key.OemPipe => "|",
+                Key.OemPlus => "+",
+                Key.OemQuestion => "/",
+                Key.OemQuotes => "\"",
+                Key.OemSemicolon => ";",
+                Key.OemTilde => "`",
+                Key.Right => "Right Arrow",
+                Key.Separator => "/",
+                Key.Subtract => "-",
+                Key.Up => "Up Arrow",
+                _ => key.ToString(),
+            };
+        }
+
+        private static string ToOSXString(Key key)
+        {
+            return key switch
+            {
+                Key.Back => "⌫",
+                Key.Down => "↓",
+                Key.End => "↘",
+                Key.Escape => "⎋",
+                Key.Home => "↖",
+                Key.Left => "←",
+                Key.Return => "↩",
+                Key.PageDown => "⇞",
+                Key.PageUp => "⇟",
+                Key.Right => "→",
+                Key.Space => "␣",
+                Key.Tab => "⇥",
+                Key.Up => "↑",
+                _ => ToString(key),
+            };
+        }
+    }
+}

+ 47 - 0
src/Avalonia.Controls/MenuItem.cs

@@ -13,6 +13,7 @@ using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -48,6 +49,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<object> IconProperty =
             AvaloniaProperty.Register<MenuItem, object>(nameof(Icon));
 
+        /// <summary>
+        /// Defines the <see cref="InputGesture"/> property.
+        /// </summary>
+        public static readonly StyledProperty<KeyGesture> InputGestureProperty =
+            AvaloniaProperty.Register<MenuItem, KeyGesture>(nameof(InputGesture));
+
         /// <summary>
         /// Defines the <see cref="IsSelected"/> property.
         /// </summary>
@@ -93,6 +100,7 @@ namespace Avalonia.Controls
         private ICommand _command;
         private bool _commandCanExecute = true;
         private Popup _popup;
+        private IDisposable _gridHack;
 
         /// <summary>
         /// Initializes static members of the <see cref="MenuItem"/> class.
@@ -194,6 +202,19 @@ namespace Avalonia.Controls
             set { SetValue(IconProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the input gesture that will be displayed in the menu item.
+        /// </summary>
+        /// <remarks>
+        /// Setting this property does not cause the input gesture to be handled by the menu item,
+        /// it simply displays the gesture text in the menu.
+        /// </remarks>
+        public KeyGesture InputGesture
+        {
+            get { return GetValue(InputGestureProperty); }
+            set { SetValue(InputGestureProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
         /// </summary>
@@ -304,6 +325,32 @@ namespace Avalonia.Controls
             {
                 Command.CanExecuteChanged -= CanExecuteChanged;
             }
+
+            _gridHack?.Dispose();
+            _gridHack = null;
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+
+            if (this.GetVisualParent() is IControl parent)
+            {
+                // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
+                // property to store SharedSizeGroup state, except property inheritance is done
+                // down the logical tree. In this case, the control which is setting
+                // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
+                // the way Grid stores shared size state, the developers of WPF just created a
+                // binding of the internal state of the visual parent to the menu item. We don't
+                // have much choice but to do the same for now unless we want to refactor Grid,
+                // which I honestly am not brave enough to do right now. Here's the same hack in
+                // the WPF codebase:
+                //
+                // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
+                _gridHack = Bind(
+                    DefinitionBase.PrivateSharedSizeScopeProperty,
+                    parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
+            }
         }
 
         /// <summary>

+ 1 - 1
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -36,7 +36,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        void SetSystemDecorations(bool enabled);
+        void SetSystemDecorations(SystemDecorations enabled);
 
         /// <summary>
         /// Sets the icon of this window.

+ 0 - 11
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives
             return base.CreateTextLayout(constraint, StripAccessKey(text));
         }
 
-        /// <summary>
-        /// Measures the control.
-        /// </summary>
-        /// <param name="availableSize">The available size for the control.</param>
-        /// <returns>The desired size.</returns>
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            var result = base.MeasureOverride(availableSize);
-            return result.WithHeight(result.Height + 1);
-        }
-
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {

+ 1 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
+            DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
 
             DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
 

+ 45 - 15
src/Avalonia.Controls/TextBlock.cs

@@ -20,6 +20,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IBrush> BackgroundProperty =
             Border.BackgroundProperty.AddOwner<TextBlock>();
 
+        /// <summary>
+        /// Defines the <see cref="Padding"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Thickness> PaddingProperty =
+            Decorator.PaddingProperty.AddOwner<TextBlock>();
+
         // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner
         // them into TextBlock.
 
@@ -29,7 +35,7 @@ namespace Avalonia.Controls
         public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
             AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>(
                 nameof(FontFamily),
-                defaultValue:  FontFamily.Default,
+                defaultValue: FontFamily.Default,
                 inherits: true);
 
         /// <summary>
@@ -110,20 +116,31 @@ namespace Avalonia.Controls
         static TextBlock()
         {
             ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
+
             AffectsRender<TextBlock>(
-                BackgroundProperty,
-                ForegroundProperty,
-                FontWeightProperty,
-                FontSizeProperty,
-                FontStyleProperty);
+                BackgroundProperty, ForegroundProperty, FontSizeProperty, 
+                FontWeightProperty, FontStyleProperty, TextWrappingProperty, 
+                TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, 
+                TextDecorationsProperty, TextProperty, PaddingProperty);
+
+            AffectsMeasure<TextBlock>(
+                FontSizeProperty, FontWeightProperty, FontStyleProperty, 
+                FontFamilyProperty, TextTrimmingProperty, TextProperty,
+                PaddingProperty);
 
             Observable.Merge(
                 TextProperty.Changed,
+                ForegroundProperty.Changed,
                 TextAlignmentProperty.Changed,
+                TextWrappingProperty.Changed,
+                TextTrimmingProperty.Changed,
                 FontSizeProperty.Changed,
                 FontStyleProperty.Changed,
-                FontWeightProperty.Changed
-            ).AddClassHandler<TextBlock>((x, _) => x.OnTextPropertiesChanged());
+                FontWeightProperty.Changed,
+                FontFamilyProperty.Changed,
+                TextDecorationsProperty.Changed,
+                PaddingProperty.Changed
+            ).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
         }
 
         /// <summary>
@@ -145,6 +162,15 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Gets or sets the padding to place around the <see cref="Text"/>.
+        /// </summary>
+        public Thickness Padding
+        {
+            get { return GetValue(PaddingProperty); }
+            set { SetValue(PaddingProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a brush used to paint the control's background.
         /// </summary>
@@ -363,7 +389,9 @@ namespace Avalonia.Controls
                 context.FillRectangle(background, new Rect(Bounds.Size));
             }
 
-            TextLayout?.Draw(context.PlatformImpl, new Point());
+            var padding = Padding;
+
+            TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
         }
 
         /// <summary>
@@ -412,6 +440,10 @@ namespace Avalonia.Controls
                 return new Size();
             }
 
+            var padding = Padding;
+
+            availableSize = availableSize.Deflate(padding);
+
             if (_constraint != availableSize)
             {
                 InvalidateTextLayout();
@@ -419,19 +451,17 @@ namespace Avalonia.Controls
 
             _constraint = availableSize;
 
-            return TextLayout?.Bounds.Size ?? Size.Empty;
+            var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
+
+            return measuredSize.Inflate(padding);
         }
 
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            InvalidateTextLayout();
-            InvalidateMeasure();
-        }
 
-        private void OnTextPropertiesChanged()
-        {
             InvalidateTextLayout();
+
             InvalidateMeasure();
         }
     }

+ 37 - 4
src/Avalonia.Controls/TextBox.cs

@@ -275,6 +275,22 @@ namespace Avalonia.Controls
             }
         }
 
+        public string SelectedText
+        {
+            get { return GetSelection(); }
+            set
+            {
+                if (value == null)
+                {
+                    return;
+                }
+
+                _undoRedoHelper.Snapshot();
+                HandleTextInput(value);
+                _undoRedoHelper.Snapshot();
+            }
+        }
+
         /// <summary>
         /// Gets or sets the horizontal alignment of the content within the control.
         /// </summary>
@@ -683,12 +699,12 @@ namespace Avalonia.Controls
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
-            var point = e.GetPosition(_presenter);
-            var index = CaretIndex = _presenter.GetCaretIndex(point);
             var text = Text;
 
-            if (text != null && e.MouseButton == MouseButton.Left)
+            if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
+                var point = e.GetPosition(_presenter);
+                var index = CaretIndex = _presenter.GetCaretIndex(point);
                 switch (e.ClickCount)
                 {
                     case 1:
@@ -714,7 +730,8 @@ namespace Avalonia.Controls
 
         protected override void OnPointerMoved(PointerEventArgs e)
         {
-            if (_presenter != null && e.Pointer.Captured == _presenter)
+            // selection should not change during pointer move if the user right clicks
+            if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
                 var point = e.GetPosition(_presenter);
 
@@ -727,6 +744,22 @@ namespace Avalonia.Controls
         {
             if (_presenter != null && e.Pointer.Captured == _presenter)
             {
+                if (e.InitialPressMouseButton == MouseButton.Right)
+                {
+                    var point = e.GetPosition(_presenter);
+                    var caretIndex = _presenter.GetCaretIndex(point);
+
+                    // see if mouse clicked inside current selection
+                    // if it did not, we change the selection to where the user clicked
+                    var firstSelection = Math.Min(SelectionStart, SelectionEnd);
+                    var lastSelection = Math.Max(SelectionStart, SelectionEnd);
+                    var didClickInSelection = SelectionStart != SelectionEnd && 
+                        caretIndex >= firstSelection && caretIndex <= lastSelection;
+                    if (!didClickInSelection)
+                    {
+                        CaretIndex = SelectionEnd = SelectionStart = caretIndex;
+                    }
+                }
                 e.Pointer.Capture(null);
             }
         }

+ 16 - 4
src/Avalonia.Controls/TopLevel.cs

@@ -50,6 +50,7 @@ namespace Avalonia.Controls
         private readonly IAccessKeyHandler _accessKeyHandler;
         private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
         private readonly IPlatformRenderInterface _renderInterface;
+        private readonly IGlobalStyles _globalStyles;
         private Size _clientSize;
         private ILayoutManager _layoutManager;
 
@@ -94,6 +95,7 @@ namespace Avalonia.Controls
             _inputManager = TryGetService<IInputManager>(dependencyResolver);
             _keyboardNavigationHandler = TryGetService<IKeyboardNavigationHandler>(dependencyResolver);
             _renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
+            _globalStyles = TryGetService<IGlobalStyles>(dependencyResolver);
 
             Renderer = impl.CreateRenderer(this);
 
@@ -112,6 +114,13 @@ namespace Avalonia.Controls
 
             _keyboardNavigationHandler?.SetOwner(this);
             _accessKeyHandler?.SetOwner(this);
+
+            if (_globalStyles is object)
+            {
+                _globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded;
+                _globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved;
+            }
+
             styler?.ApplyStyles(this);
 
             ClientSize = impl.ClientSize;
@@ -215,10 +224,7 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1;
 
-        IStyleHost IStyleHost.StylingParent
-        {
-            get { return AvaloniaLocator.Current.GetService<IGlobalStyles>(); }
-        }
+        IStyleHost IStyleHost.StylingParent => _globalStyles;
 
         IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget();
 
@@ -267,6 +273,12 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual void HandleClosed()
         {
+            if (_globalStyles is object)
+            {
+                _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded;
+                _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved;
+            }
+
             var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
             ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
 

+ 82 - 14
src/Avalonia.Controls/Window.cs

@@ -2,19 +2,19 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
+using System.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform;
+using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Styling;
-using System.Collections.Generic;
-using System.Linq;
 using JetBrains.Annotations;
-using System.ComponentModel;
-using Avalonia.Interactivity;
 
 namespace Avalonia.Controls
 {
@@ -45,6 +45,27 @@ namespace Avalonia.Controls
         WidthAndHeight = 3,
     }
 
+    /// <summary>
+    /// Determines system decorations (title bar, border, etc) for a <see cref="Window"/>
+    /// </summary>
+    public enum SystemDecorations
+    {
+        /// <summary>
+        /// No decorations
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Window border without titlebar
+        /// </summary>
+        BorderOnly = 1,
+
+        /// <summary>
+        /// Fully decorated (default)
+        /// </summary>
+        Full = 2
+    }
+
     /// <summary>
     /// A top-level window.
     /// </summary>
@@ -59,8 +80,18 @@ namespace Avalonia.Controls
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        public static readonly StyledProperty<bool> HasSystemDecorationsProperty =
-            AvaloniaProperty.Register<Window, bool>(nameof(HasSystemDecorations), true);
+        [Obsolete("Use SystemDecorationsProperty instead")]
+        public static readonly DirectProperty<Window, bool> HasSystemDecorationsProperty =
+            AvaloniaProperty.RegisterDirect<Window, bool>(
+                nameof(HasSystemDecorations),
+                o => o.HasSystemDecorations,
+                (o, v) => o.HasSystemDecorations = v);
+
+        /// <summary>
+        /// Defines the <see cref="SystemDecorations"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SystemDecorations> SystemDecorationsProperty =
+            AvaloniaProperty.Register<Window, SystemDecorations>(nameof(SystemDecorations), SystemDecorations.Full);
 
         /// <summary>
         /// Enables or disables the taskbar icon
@@ -124,9 +155,6 @@ namespace Avalonia.Controls
         {
             BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
             TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
-            HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
-                (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
-
             ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
 
             IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
@@ -135,12 +163,11 @@ namespace Avalonia.Controls
 
             WindowStateProperty.Changed.AddClassHandler<Window>(
                 (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
-            
+
             MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
             MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
             MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
             MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
-
         }
 
         /// <summary>
@@ -191,11 +218,30 @@ namespace Avalonia.Controls
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        /// 
+        [Obsolete("Use SystemDecorations instead")]
         public bool HasSystemDecorations
         {
-            get { return GetValue(HasSystemDecorationsProperty); }
-            set { SetValue(HasSystemDecorationsProperty, value); }
+            get => SystemDecorations == SystemDecorations.Full;
+            set
+            {
+                var oldValue = HasSystemDecorations;
+
+                if (oldValue != value)
+                {
+                    SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None;
+                    RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Sets the system decorations (title bar, border, etc)
+        /// </summary>
+        /// 
+        public SystemDecorations SystemDecorations
+        {
+            get { return GetValue(SystemDecorationsProperty); }
+            set { SetValue(SystemDecorationsProperty, value); }
         }
 
         /// <summary>
@@ -584,5 +630,27 @@ namespace Avalonia.Controls
         /// <see cref="Closing"/> event needs to be raised.
         /// </remarks>
         protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
+
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            if (property == SystemDecorationsProperty)
+            {
+                var typedNewValue = newValue.GetValueOrDefault<SystemDecorations>();
+
+                PlatformImpl?.SetSystemDecorations(typedNewValue);
+
+                var o = oldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
+                var n = typedNewValue == SystemDecorations.Full;
+
+                if (o != n)
+                {
+                    RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
+                }
+            }
+        }
     }
 }

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics
                 }
             }
 
-            return root.AddHandler(
+            return root.AddDisposableHandler(
                 InputElement.KeyDownEvent,
                 PreviewKeyDown,
                 RoutingStrategies.Tunnel);

+ 36 - 8
src/Avalonia.Input/KeyGesture.cs

@@ -1,9 +1,11 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
+// Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
 
 namespace Avalonia.Input
 {
@@ -108,19 +110,43 @@ namespace Avalonia.Input
 
         public override string ToString()
         {
-            var parts = new List<string>();
+            var s = new StringBuilder();
 
-            foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast<KeyModifiers>())
+            static void Plus(StringBuilder s)
             {
-                if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None)
+                if (s.Length > 0)
                 {
-                    parts.Add(flag.ToString());
+                    s.Append("+");
                 }
             }
 
-            parts.Add(Key.ToString());
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Control))
+            {
+                s.Append("Ctrl");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
+            {
+                Plus(s);
+                s.Append("Shift");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
+            {
+                Plus(s);
+                s.Append("Alt");
+            }
+
+            if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
+            {
+                Plus(s);
+                s.Append("Cmd");
+            }
+
+            Plus(s);
+            s.Append(Key);
 
-            return string.Join(" + ", parts);
+            return s.ToString();
         }
 
         public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers;
@@ -141,7 +167,9 @@ namespace Avalonia.Input
                 return KeyModifiers.Control;
             }
 
-            if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase))
+            if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+                modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+                modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase))
             {
                 return KeyModifiers.Meta;
             }

+ 3 - 3
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@@ -1,6 +1,8 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <Nullable>Enable</Nullable>
+    <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@@ -9,6 +11,4 @@
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
   </ItemGroup>
   <Import Project="..\..\build\Rx.props" />
-</Project>
-
-
+</Project>

+ 200 - 0
src/Avalonia.Interactivity/EventRoute.cs

@@ -0,0 +1,200 @@
+using System;
+using Avalonia.Collections.Pooled;
+
+namespace Avalonia.Interactivity
+{
+    /// <summary>
+    /// Holds the route for a routed event and supports raising an event on that route.
+    /// </summary>
+    public class EventRoute : IDisposable
+    {
+        private readonly RoutedEvent _event;
+        private PooledList<RouteItem>? _route;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RoutedEvent"/> class.
+        /// </summary>
+        /// <param name="e">The routed event to be raised.</param>
+        public EventRoute(RoutedEvent e)
+        {
+            e = e ?? throw new ArgumentNullException(nameof(e));
+
+            _event = e;
+            _route = null;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the route has any handlers.
+        /// </summary>
+        public bool HasHandlers => _route?.Count > 0;
+
+        /// <summary>
+        /// Adds a handler to the route.
+        /// </summary>
+        /// <param name="target">The target on which the event should be raised.</param>
+        /// <param name="handler">The handler for the event.</param>
+        /// <param name="routes">The routing strategies to listen to.</param>
+        /// <param name="handledEventsToo">
+        /// If true the handler will be raised even when the routed event is marked as handled.
+        /// </param>
+        /// <param name="adapter">
+        /// An optional adapter which if supplied, will be called with <paramref name="handler"/>
+        /// and the parameters for the event. This adapter can be used to avoid calling
+        /// `DynamicInvoke` on the handler.
+        /// </param>
+        public void Add(
+            IInteractive target,
+            Delegate handler,
+            RoutingStrategies routes,
+            bool handledEventsToo = false,
+            Action<Delegate, object, RoutedEventArgs>? adapter = null)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
+
+            _route ??= new PooledList<RouteItem>(16);
+            _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo));
+        }
+
+        /// <summary>
+        /// Adds a class handler to the route.
+        /// </summary>
+        /// <param name="target">The target on which the event should be raised.</param>
+        public void AddClassHandler(IInteractive target)
+        {
+            target = target ?? throw new ArgumentNullException(nameof(target));
+
+            _route ??= new PooledList<RouteItem>(16);
+            _route.Add(new RouteItem(target, null, null, 0, false));
+        }
+
+        /// <summary>
+        /// Raises an event along the route.
+        /// </summary>
+        /// <param name="source">The event source.</param>
+        /// <param name="e">The event args.</param>
+        public void RaiseEvent(IInteractive source, RoutedEventArgs e)
+        {
+            source = source ?? throw new ArgumentNullException(nameof(source));
+            e = e ?? throw new ArgumentNullException(nameof(e));
+
+            e.Source = source;
+
+            if (_event.RoutingStrategies == RoutingStrategies.Direct)
+            {
+                e.Route = RoutingStrategies.Direct;
+                RaiseEventImpl(e);
+                _event.InvokeRouteFinished(e);
+            }
+            else
+            {
+                if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
+                {
+                    e.Route = RoutingStrategies.Tunnel;
+                    RaiseEventImpl(e);
+                    _event.InvokeRouteFinished(e);
+                }
+
+                if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble))
+                {
+                    e.Route = RoutingStrategies.Bubble;
+                    RaiseEventImpl(e);
+                    _event.InvokeRouteFinished(e);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Disposes of the event route.
+        /// </summary>
+        public void Dispose()
+        {
+            _route?.Dispose();
+            _route = null;
+        }
+
+        private void RaiseEventImpl(RoutedEventArgs e)
+        {
+            if (_route is null)
+            {
+                return;
+            }
+
+            if (e.Source is null)
+            {
+                throw new ArgumentException("Event source may not be null", nameof(e));
+            }
+
+            IInteractive? lastTarget = null;
+            var start = 0;
+            var end = _route.Count;
+            var step = 1;
+
+            if (e.Route == RoutingStrategies.Tunnel)
+            {
+                start = end - 1;
+                step = end = -1;
+            }
+
+            for (var i = start; i != end; i += step)
+            {
+                var entry = _route[i];
+
+                // If we've got to a new control then call any RoutedEvent.Raised listeners.
+                if (entry.Target != lastTarget)
+                {
+                    if (!e.Handled)
+                    {
+                        _event.InvokeRaised(entry.Target, e);
+                    }
+
+                    // If this is a direct event and we've already raised events then we're finished.
+                    if (e.Route == RoutingStrategies.Direct && lastTarget is object)
+                    {
+                        return;
+                    }
+
+                    lastTarget = entry.Target;
+                }
+
+                // Raise the event handler.
+                if (entry.Handler is object &&
+                    entry.Routes.HasFlagCustom(e.Route) &&
+                    (!e.Handled || entry.HandledEventsToo))
+                {
+                    if (entry.Adapter is object)
+                    {
+                        entry.Adapter(entry.Handler, entry.Target, e);
+                    }
+                    else
+                    {
+                        entry.Handler.DynamicInvoke(entry.Target, e);
+                    }
+                }
+            }
+        }
+
+        private readonly struct RouteItem
+        {
+            public RouteItem(
+                IInteractive target,
+                Delegate? handler,
+                Action<Delegate, object, RoutedEventArgs>? adapter,
+                RoutingStrategies routes,
+                bool handledEventsToo)
+            {
+                Target = target;
+                Handler = handler;
+                Adapter = adapter;
+                Routes = routes;
+                HandledEventsToo = handledEventsToo;
+            }
+
+            public IInteractive Target { get; }
+            public Delegate? Handler { get; }
+            public Action<Delegate, object, RoutedEventArgs>? Adapter { get; }
+            public RoutingStrategies Routes { get; }
+            public bool HandledEventsToo { get; }
+        }
+    }
+}

+ 0 - 20
src/Avalonia.Interactivity/EventSubscription.cs

@@ -1,20 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-
-namespace Avalonia.Interactivity
-{
-    internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
-
-    internal class EventSubscription
-    {
-        public HandlerInvokeSignature InvokeAdapter { get; set; }
-
-        public Delegate Handler { get; set; }
-
-        public RoutingStrategies Routes { get; set; }
-
-        public bool AlsoIfHandled { get; set; }
-    }
-}

+ 10 - 3
src/Avalonia.Interactivity/IInteractive.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Interactivity
         /// <summary>
         /// Gets the interactive parent of the object for bubbling and tunneling events.
         /// </summary>
-        IInteractive InteractiveParent { get; }
+        IInteractive? InteractiveParent { get; }
 
         /// <summary>
         /// Adds a handler for the specified routed event.
@@ -23,7 +23,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler(
+        void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -38,7 +38,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler<TEventArgs>(
+        void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -60,6 +60,13 @@ namespace Avalonia.Interactivity
         void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
             where TEventArgs : RoutedEventArgs;
 
+        /// <summary>
+        /// Adds the object's handlers for a routed event to an event route.
+        /// </summary>
+        /// <param name="routedEvent">The event.</param>
+        /// <param name="route">The event route.</param>
+        void AddToEventRoute(RoutedEvent routedEvent, EventRoute route);
+
         /// <summary>
         /// Raises a routed event.
         /// </summary>

+ 96 - 197
src/Avalonia.Interactivity/Interactive.cs

@@ -3,8 +3,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
 using Avalonia.Layout;
 using Avalonia.VisualTree;
 
@@ -15,16 +13,12 @@ namespace Avalonia.Interactivity
     /// </summary>
     public class Interactive : Layoutable, IInteractive
     {
-        private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
-
-        private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
+        private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
 
         /// <summary>
         /// Gets the interactive parent of the object for bubbling and tunneling events.
         /// </summary>
-        IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
-
-        private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
+        IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
 
         /// <summary>
         /// Adds a handler for the specified routed event.
@@ -33,24 +27,18 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler(
+        public void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
             bool handledEventsToo = false)
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(handler != null);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
-            var subscription = new EventSubscription
-            {
-                Handler = handler,
-                Routes = routes,
-                AlsoIfHandled = handledEventsToo,
-            };
+            var subscription = new EventSubscription(handler, routes, handledEventsToo);
 
-            return AddEventSubscription(routedEvent, subscription);
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -61,44 +49,26 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler<TEventArgs>(
+        public void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
             bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(handler != null);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
-            // EventHandler delegate is not covariant, this forces us to create small wrapper
-            // that will cast our type erased instance and invoke it.
-            Type eventArgsType = routedEvent.EventArgsType;
-
-            if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter))
+            static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
             {
-                void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
-                {
-                    var typedHandler = (EventHandler<TEventArgs>)baseHandler;
-                    var typedArgs = (TEventArgs)args;
+                var typedHandler = (EventHandler<TEventArgs>)baseHandler;
+                var typedArgs = (TEventArgs)args;
 
-                    typedHandler(sender, typedArgs);
-                }
-
-                invokeAdapter = InvokeAdapter;
-
-                s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
+                typedHandler(sender, typedArgs);
             }
 
-            var subscription = new EventSubscription
-            {
-                InvokeAdapter = invokeAdapter,
-                Handler = handler,
-                Routes = routes,
-                AlsoIfHandled = handledEventsToo,
-            };
+            var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
 
-            return AddEventSubscription(routedEvent, subscription);
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -108,14 +78,19 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
         {
-            Contract.Requires<ArgumentNullException>(routedEvent != null);
-            Contract.Requires<ArgumentNullException>(handler != null);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
-            List<EventSubscription> subscriptions = null;
-
-            if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true)
+            if (_eventHandlers is object &&
+                _eventHandlers.TryGetValue(routedEvent, out var subscriptions))
             {
-                subscriptions.RemoveAll(x => x.Handler == handler);
+                for (var i = subscriptions.Count - 1; i >= 0; i--)
+                {
+                    if (subscriptions[i].Handler == handler)
+                    {
+                        subscriptions.RemoveAt(i);
+                    }
+                }
             }
         }
 
@@ -137,191 +112,115 @@ namespace Avalonia.Interactivity
         /// <param name="e">The event args.</param>
         public void RaiseEvent(RoutedEventArgs e)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Source = e.Source ?? this;
-
-            if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct)
-            {
-                e.Route = RoutingStrategies.Direct;
-                RaiseEventImpl(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
-            }
+            e = e ?? throw new ArgumentNullException(nameof(e));
 
-            if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
+            if (e.RoutedEvent == null)
             {
-                TunnelEvent(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
+                throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
             }
 
-            if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0)
-            {
-                BubbleEvent(e);
-                e.RoutedEvent.InvokeRouteFinished(e);
-            }
+            using var route = BuildEventRoute(e.RoutedEvent);
+            route.RaiseEvent(this, e);
         }
 
-        /// <summary>
-        /// Bubbles an event.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        private void BubbleEvent(RoutedEventArgs e)
-        {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Route = RoutingStrategies.Bubble;
-
-            var traverser = HierarchyTraverser<RaiseEventTraverse, NopTraverse>.Create(e);
-
-            traverser.Traverse(this);
-        }
-
-        /// <summary>
-        /// Tunnels an event.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        private void TunnelEvent(RoutedEventArgs e)
+        void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
-
-            e.Route = RoutingStrategies.Tunnel;
-
-            var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.Create(e);
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+            route = route ?? throw new ArgumentNullException(nameof(route));
 
-            traverser.Traverse(this);
+            if (_eventHandlers != null &&
+                _eventHandlers.TryGetValue(routedEvent, out var subscriptions))
+            {
+                foreach (var sub in subscriptions)
+                {
+                    route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter);
+                }
+            }
         }
 
         /// <summary>
-        /// Carries out the actual invocation of an event on this object.
+        /// Builds an event route for a routed event.
         /// </summary>
-        /// <param name="e">The event args.</param>
-        private void RaiseEventImpl(RoutedEventArgs e)
+        /// <param name="e">The routed event.</param>
+        /// <returns>An <see cref="EventRoute"/> describing the route.</returns>
+        /// <remarks>
+        /// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> is sufficent to raise a routed
+        /// event, however there are situations in which the construction of the event args is expensive
+        /// and should be avoided if there are no handlers for an event. In these cases you can call
+        /// this method to build the event route and check the <see cref="EventRoute.HasHandlers"/>
+        /// property to see if there are any handlers registered on the route. If there are, call
+        /// <see cref="EventRoute.RaiseEvent(IInteractive, RoutedEventArgs)"/> to raise the event.
+        /// </remarks>
+        protected EventRoute BuildEventRoute(RoutedEvent e)
         {
-            Contract.Requires<ArgumentNullException>(e != null);
+            e = e ?? throw new ArgumentNullException(nameof(e));
 
-            e.RoutedEvent.InvokeRaised(this, e);
+            var result = new EventRoute(e);
+            var hasClassHandlers = e.HasRaisedSubscriptions;
 
-            List<EventSubscription> subscriptions = null;
-
-            if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true)
+            if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) ||
+                e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
             {
-                foreach (var sub in subscriptions.ToList())
-                {
-                    bool correctRoute = (e.Route & sub.Routes) != 0;
-                    bool notFinished = !e.Handled || sub.AlsoIfHandled;
+                IInteractive? element = this;
 
-                    if (correctRoute && notFinished)
+                while (element != null)
+                {
+                    if (hasClassHandlers)
                     {
-                        if (sub.InvokeAdapter != null)
-                        {
-                            sub.InvokeAdapter(sub.Handler, this, e);
-                        }
-                        else
-                        {
-                            sub.Handler.DynamicInvoke(this, e);
-                        }
+                        result.AddClassHandler(element);
                     }
+
+                    element.AddToEventRoute(e, result);
+                    element = element.InteractiveParent;
                 }
             }
-        }
-
-        private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
-        {
-            if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
+            else
             {
-                subscriptions = new List<EventSubscription>();
-                EventHandlers.Add(routedEvent, subscriptions);
-            }
-
-            return subscriptions;
-        }
-
-        private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
-        {
-            List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
-
-            subscriptions.Add(subscription);
-
-            return new UnsubscribeDisposable(subscriptions, subscription);
-        }
-
-        private sealed class UnsubscribeDisposable : IDisposable
-        {
-            private readonly List<EventSubscription> _subscriptions;
-            private readonly EventSubscription _subscription;
+                if (hasClassHandlers)
+                {
+                    result.AddClassHandler(this);
+                }
 
-            public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
-            {
-                _subscriptions = subscriptions;
-                _subscription = subscription;
+                ((IInteractive)this).AddToEventRoute(e, result);
             }
 
-            public void Dispose()
-            {
-                _subscriptions.Remove(_subscription);
-            }
+            return result;
         }
 
-        private interface ITraverse
+        private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
         {
-            void Execute(IInteractive target, RoutedEventArgs e);
-        }
+            _eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
 
-        private struct NopTraverse : ITraverse
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            public void Execute(IInteractive target, RoutedEventArgs e)
+            if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
             {
+                subscriptions = new List<EventSubscription>();
+                _eventHandlers.Add(routedEvent, subscriptions);
             }
-        }
 
-        private struct RaiseEventTraverse : ITraverse
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            public void Execute(IInteractive target, RoutedEventArgs e)
-            {
-                ((Interactive)target).RaiseEventImpl(e);
-            }
+            subscriptions.Add(subscription);
         }
 
-        /// <summary>
-        /// Traverses interactive hierarchy allowing for raising events.
-        /// </summary>
-        /// <typeparam name="TPreTraverse">Called before parent is traversed.</typeparam>
-        /// <typeparam name="TPostTraverse">Called after parent has been traversed.</typeparam>
-        private struct HierarchyTraverser<TPreTraverse, TPostTraverse>
-            where TPreTraverse : struct, ITraverse
-            where TPostTraverse : struct, ITraverse
+        private readonly struct EventSubscription
         {
-            private TPreTraverse _preTraverse;
-            private TPostTraverse _postTraverse;
-            private readonly RoutedEventArgs _args;
-
-            private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args)
-            {
-                _preTraverse = preTraverse;
-                _postTraverse = postTraverse;
-                _args = args;
-            }
-
-            public static HierarchyTraverser<TPreTraverse, TPostTraverse> Create(RoutedEventArgs args)
-            {
-                return new HierarchyTraverser<TPreTraverse, TPostTraverse>(new TPreTraverse(), new TPostTraverse(), args);
+            public EventSubscription(
+                Delegate handler,
+                RoutingStrategies routes,
+                bool handledEventsToo,
+                Action<Delegate, object, RoutedEventArgs>? invokeAdapter = null)
+            {
+                Handler = handler;
+                Routes = routes;
+                HandledEventsToo = handledEventsToo;
+                InvokeAdapter = invokeAdapter;
             }
 
-            public void Traverse(IInteractive target)
-            {
-                _preTraverse.Execute(target, _args);
+            public Action<Delegate, object, RoutedEventArgs>? InvokeAdapter { get; }
 
-                IInteractive parent = target.InteractiveParent;
+            public Delegate Handler { get; }
 
-                if (parent != null)
-                {
-                    Traverse(parent);
-                }
+            public RoutingStrategies Routes { get; }
 
-                _postTraverse.Execute(target, _args);
-            }
+            public bool HandledEventsToo { get; }
         }
     }
 }

+ 27 - 3
src/Avalonia.Interactivity/InteractiveExtensions.cs

@@ -2,8 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Reactive.Disposables;
 using System.Reactive.Linq;
 
 namespace Avalonia.Interactivity
@@ -13,6 +12,28 @@ namespace Avalonia.Interactivity
     /// </summary>
     public static class InteractiveExtensions
     {
+        /// <summary>
+        /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription.
+        /// </summary>
+        /// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
+        /// <param name="o">Target for adding given event handler.</param>
+        /// <param name="routedEvent">The routed event.</param>
+        /// <param name="handler">The handler.</param>
+        /// <param name="routes">The routing strategies to listen to.</param>
+        /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
+        /// <returns>A disposable that terminates the event subscription.</returns>
+        public static IDisposable AddDisposableHandler<TEventArgs>(this IInteractive o, RoutedEvent<TEventArgs> routedEvent,
+            EventHandler<TEventArgs> handler,
+            RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
+            bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
+        {
+            o.AddHandler(routedEvent, handler, routes, handledEventsToo);
+
+            return Disposable.Create(
+                (instance: o, handler, routedEvent),
+                state => state.instance.RemoveHandler(state.routedEvent, state.handler));
+        }
+
         /// <summary>
         /// Gets an observable for a <see cref="RoutedEvent{TEventArgs}"/>.
         /// </summary>
@@ -30,7 +51,10 @@ namespace Avalonia.Interactivity
             bool handledEventsToo = false)
                 where TEventArgs : RoutedEventArgs
         {
-            return Observable.Create<TEventArgs>(x => o.AddHandler(
+            o = o ?? throw new ArgumentNullException(nameof(o));
+            routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
+
+            return Observable.Create<TEventArgs>(x => o.AddDisposableHandler(
                 routedEvent, 
                 (_, e) => x.OnNext(e), 
                 routes,

+ 12 - 8
src/Avalonia.Interactivity/RoutedEvent.cs

@@ -25,10 +25,14 @@ namespace Avalonia.Interactivity
             Type eventArgsType,
             Type ownerType)
         {
-            Contract.Requires<ArgumentNullException>(name != null);
-            Contract.Requires<ArgumentNullException>(eventArgsType != null);
-            Contract.Requires<ArgumentNullException>(ownerType != null);
-            Contract.Requires<InvalidCastException>(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType));
+            name = name ?? throw new ArgumentNullException(nameof(name));
+            eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name));
+            ownerType = ownerType ?? throw new ArgumentNullException(nameof(name));
+
+            if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType))
+            {
+                throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs.");
+            }
 
             EventArgsType = eventArgsType;
             Name = name;
@@ -44,6 +48,8 @@ namespace Avalonia.Interactivity
 
         public RoutingStrategies RoutingStrategies { get; }
 
+        public bool HasRaisedSubscriptions => _raised.HasObservers;
+
         public IObservable<(object, RoutedEventArgs)> Raised => _raised;
         public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
 
@@ -52,7 +58,7 @@ namespace Avalonia.Interactivity
             RoutingStrategies routingStrategy)
                 where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(name != null);
+            name = name ?? throw new ArgumentNullException(nameof(name));
 
             var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
             RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent);
@@ -65,7 +71,7 @@ namespace Avalonia.Interactivity
             Type ownerType)
                 where TEventArgs : RoutedEventArgs
         {
-            Contract.Requires<ArgumentNullException>(name != null);
+            name = name ?? throw new ArgumentNullException(nameof(name));
 
             var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
             RoutedEventRegistry.Instance.Register(ownerType, routedEvent);
@@ -108,8 +114,6 @@ namespace Avalonia.Interactivity
         public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
             : base(name, routingStrategies, typeof(TEventArgs), ownerType)
         {
-            Contract.Requires<ArgumentNullException>(name != null);
-            Contract.Requires<ArgumentNullException>(ownerType != null);
         }
 
         [Obsolete("Use overload taking Action<TTarget, TEventArgs>.")]

+ 4 - 4
src/Avalonia.Interactivity/RoutedEventArgs.cs

@@ -11,12 +11,12 @@ namespace Avalonia.Interactivity
         {
         }
 
-        public RoutedEventArgs(RoutedEvent routedEvent)
+        public RoutedEventArgs(RoutedEvent? routedEvent)
         {
             RoutedEvent = routedEvent;
         }
 
-        public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source)
+        public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
         {
             RoutedEvent = routedEvent;
             Source = source;
@@ -24,10 +24,10 @@ namespace Avalonia.Interactivity
 
         public bool Handled { get; set; }
 
-        public RoutedEvent RoutedEvent { get; set; }
+        public RoutedEvent? RoutedEvent { get; set; }
 
         public RoutingStrategies Route { get; set; }
 
-        public IInteractive Source { get; set; }
+        public IInteractive? Source { get; set; }
     }
 }

+ 3 - 3
src/Avalonia.Interactivity/RoutedEventRegistry.cs

@@ -32,8 +32,8 @@ namespace Avalonia.Interactivity
         /// </remarks>
         public void Register(Type type, RoutedEvent @event)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
-            Contract.Requires<ArgumentNullException>(@event != null);
+            type = type ?? throw new ArgumentNullException(nameof(type));
+            @event = @event ?? throw new ArgumentNullException(nameof(@event));
 
             if (!_registeredRoutedEvents.TryGetValue(type, out var list))
             {
@@ -66,7 +66,7 @@ namespace Avalonia.Interactivity
         /// <returns>All routed events registered with the provided type.</returns>
         public IReadOnlyList<RoutedEvent> GetRegistered(Type type)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
+            type = type ?? throw new ArgumentNullException(nameof(type));
 
             if (_registeredRoutedEvents.TryGetValue(type, out var events))
             {

+ 7 - 0
src/Avalonia.Layout/Layoutable.cs

@@ -507,6 +507,7 @@ namespace Avalonia.Layout
             {
                 var margin = Margin;
 
+                ApplyStyling();
                 ApplyTemplate();
 
                 var constrained = LayoutHelper.ApplyLayoutConstraints(
@@ -692,6 +693,12 @@ namespace Avalonia.Layout
             return finalSize;
         }
 
+        protected sealed override void InvalidateStyles()
+        {
+            base.InvalidateStyles();
+            InvalidateMeasure();
+        }
+
         /// <inheritdoc/>
         protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
         {

+ 2 - 2
src/Avalonia.Native/WindowImpl.cs

@@ -68,9 +68,9 @@ namespace Avalonia.Native
             _native.CanResize = value;
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(Controls.SystemDecorations enabled)
         {
-            _native.HasDecorations = enabled;
+            _native.HasDecorations = (Interop.SystemDecorations)enabled;
         }
 
         public void SetTitleBarColor (Avalonia.Media.Color color)

+ 128 - 11
src/Avalonia.Styling/StyledElement.cs

@@ -213,6 +213,7 @@ namespace Avalonia
                 {
                     _styles = new Styles(this);
                     _styles.ResourcesChanged += ThisResourcesChanged;
+                    NotifyResourcesChanged(new ResourcesChangedEventArgs());
                 }
 
                 return _styles;
@@ -352,21 +353,34 @@ namespace Avalonia
 
             if (--_initCount == 0 && _logicalRoot != null)
             {
-                InitializeStylesIfNeeded();
-
+                ApplyStyling();
                 InitializeIfNeeded();
             }
         }
 
-        private void InitializeStylesIfNeeded(bool force = false)
+        /// <summary>
+        /// Applies styling to the control if the control is initialized and styling is not
+        /// already applied.
+        /// </summary>
+        /// <returns>
+        /// A value indicating whether styling is now applied to the control.
+        /// </returns>
+        protected bool ApplyStyling()
         {
-            if (_initCount == 0 && (!_styled || force))
+            if (_initCount == 0 && !_styled)
             {
-                ApplyStyling();
+                AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
                 _styled = true;
             }
+
+            return _styled;
         }
 
+        /// <summary>
+        /// Detaches all styles from the element and queues a restyle.
+        /// </summary>
+        protected virtual void InvalidateStyles() => DetachStyles();
+
         protected void InitializeIfNeeded()
         {
             if (_initCount == 0 && !IsInitialized)
@@ -480,6 +494,21 @@ namespace Avalonia
 
         void IStyleable.DetachStyles() => DetachStyles();
 
+        void IStyleable.DetachStyles(IReadOnlyList<IStyle> styles) => DetachStyles(styles);
+
+        void IStyleable.InvalidateStyles() => InvalidateStyles();
+
+        void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
+        {
+            InvalidateStylesOnThisAndDescendents();
+        }
+
+        void IStyleHost.StylesRemoved(IReadOnlyList<IStyle> styles)
+        {
+            var allStyles = RecurseStyles(styles);
+            DetachStylesFromThisAndDescendents(allStyles);
+        }
+
         protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             switch (e.Action)
@@ -604,11 +633,6 @@ namespace Avalonia
             return null;
         }
 
-        private void ApplyStyling()
-        {
-            AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
-        }
-
         private static void ValidateLogicalChild(ILogical c)
         {
             if (c == null)
@@ -635,7 +659,7 @@ namespace Avalonia
             {
                 _logicalRoot = e.Root;
 
-                InitializeStylesIfNeeded(true);
+                ApplyStyling();
 
                 OnAttachedToLogicalTree(e);
                 AttachedToLogicalTree?.Invoke(this, e);
@@ -713,6 +737,64 @@ namespace Avalonia
 
                 _appliedStyles.Clear();
             }
+
+            _styled = false;
+        }
+
+        private void DetachStyles(IReadOnlyList<IStyle> styles)
+        {
+            styles = styles ?? throw new ArgumentNullException(nameof(styles));
+
+            if (_appliedStyles is null)
+            {
+                return;
+            }
+
+            var count = styles.Count;
+
+            for (var i = 0; i < count; ++i)
+            {
+                for (var j = _appliedStyles.Count - 1; j >= 0; --j)
+                {
+                    var applied = _appliedStyles[j];
+
+                    if (applied.Source == styles[i])
+                    {
+                        applied.Dispose();
+                        _appliedStyles.RemoveAt(j);
+                    }
+                }
+            }
+        }
+
+        private void InvalidateStylesOnThisAndDescendents()
+        {
+            InvalidateStyles();
+
+            if (_logicalChildren is object)
+            {
+                var childCount = _logicalChildren.Count;
+
+                for (var i = 0; i < childCount; ++i)
+                {
+                    (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents();
+                }
+            }
+        }
+
+        private void DetachStylesFromThisAndDescendents(IReadOnlyList<IStyle> styles)
+        {
+            DetachStyles(styles);
+
+            if (_logicalChildren is object)
+            {
+                var childCount = _logicalChildren.Count;
+
+                for (var i = 0; i < childCount; ++i)
+                {
+                    (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles);
+                }
+            }
         }
 
         private void ClearLogicalParent(IEnumerable<ILogical> children)
@@ -750,5 +832,40 @@ namespace Avalonia
         {
             NotifyResourcesChanged(e);
         }
+
+        private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
+        {
+            var count = styles.Count;
+            List<IStyle>? result = null;
+
+            for (var i = 0; i < count; ++i)
+            {
+                var style = styles[i];
+
+                if (style.Children.Count > 0)
+                {
+                    if (result is null)
+                    {
+                        result = new List<IStyle>(styles);
+                    }
+
+                    RecurseStyles(style.Children, result);
+                }
+            }
+
+            return result ?? styles;
+        }
+
+        private static void RecurseStyles(IReadOnlyList<IStyle> styles, List<IStyle> result)
+        {
+            var count = styles.Count;
+
+            for (var i = 0; i < count; ++i)
+            {
+                var style = styles[i];
+                result.Add(style);
+                RecurseStyles(style.Children, result);
+            }
+        }
     }
 }

+ 6 - 3
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators
     {
         private readonly IList<string> _match;
         private readonly IAvaloniaReadOnlyList<string> _classes;
+        private NotifyCollectionChangedEventHandler? _classesChangedHandler;
 
         public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
         {
@@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators
             _match = match;
         }
 
+        private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
+            _classesChangedHandler ??= ClassesChanged;
+
         public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
         {
             int remainingMatches = toMatch.Count;
@@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators
             return remainingMatches == 0;
         }
 
-
         protected override void Initialize()
         {
             PublishNext(IsMatching());
-            _classes.CollectionChanged += ClassesChanged;
+            _classes.CollectionChanged += ClassesChangedHandler;
         }
 
         protected override void Deinitialize()
         {
-            _classes.CollectionChanged -= ClassesChanged;
+            _classes.CollectionChanged -= ClassesChangedHandler;
         }
 
         private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)

+ 14 - 0
src/Avalonia.Styling/Styling/IGlobalStyles.cs

@@ -1,6 +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;
+
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -8,5 +13,14 @@ namespace Avalonia.Styling
     /// </summary>
     public interface IGlobalStyles : IStyleHost
     {
+        /// <summary>
+        /// Raised when styles are added to <see cref="Styles"/> or a nested styles collection.
+        /// </summary>
+        public event Action<IReadOnlyList<IStyle>> GlobalStylesAdded;
+
+        /// <summary>
+        /// Raised when styles are removed from <see cref="Styles"/> or a nested styles collection.
+        /// </summary>
+        public event Action<IReadOnlyList<IStyle>> GlobalStylesRemoved;
     }
 }

+ 7 - 1
src/Avalonia.Styling/Styling/IStyle.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.Collections.Generic;
 using Avalonia.Controls;
 
 #nullable enable
@@ -13,7 +14,12 @@ namespace Avalonia.Styling
     public interface IStyle : IResourceNode
     {
         /// <summary>
-        /// Attaches the style to a control if the style's selector matches.
+        /// Gets a collection of child styles.
+        /// </summary>
+        IReadOnlyList<IStyle> Children { get; }
+
+        /// <summary>
+        /// Attaches the style and any child styles to a control if the style's selector matches.
         /// </summary>
         /// <param name="target">The control to attach to.</param>
         /// <param name="host">The element that hosts the style.</param>

+ 16 - 0
src/Avalonia.Styling/Styling/IStyleHost.cs

@@ -2,6 +2,10 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
+using System.Collections.Generic;
+
+#nullable enable
+
 namespace Avalonia.Styling
 {
     /// <summary>
@@ -27,5 +31,17 @@ namespace Avalonia.Styling
         /// Gets the parent style host element.
         /// </summary>
         IStyleHost StylingParent { get; }
+
+        /// <summary>
+        /// Called when styles are added to <see cref="Styles"/> or a nested styles collection.
+        /// </summary>
+        /// <param name="styles">The added styles.</param>
+        void StylesAdded(IReadOnlyList<IStyle> styles);
+
+        /// <summary>
+        /// Called when styles are removed from <see cref="Styles"/> or a nested styles collection.
+        /// </summary>
+        /// <param name="styles">The removed styles.</param>
+        void StylesRemoved(IReadOnlyList<IStyle> styles);
     }
 }

+ 11 - 0
src/Avalonia.Styling/Styling/IStyleable.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.Collections.Generic;
 using Avalonia.Collections;
 
 #nullable enable
@@ -38,5 +39,15 @@ namespace Avalonia.Styling
         /// Detaches all styles applied to the element.
         /// </summary>
         void DetachStyles();
+
+        /// <summary>
+        /// Detaches a collection of styles, if applied to the element.
+        /// </summary>
+        void DetachStyles(IReadOnlyList<IStyle> styles);
+
+        /// <summary>
+        /// Detaches all styles from the element and queues a restyle.
+        /// </summary>
+        void InvalidateStyles();
     }
 }

+ 2 - 0
src/Avalonia.Styling/Styling/Style.cs

@@ -90,6 +90,8 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         bool IResourceProvider.HasResources => _resources?.Count > 0;
 
+        IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
+
         /// <inheritdoc/>
         public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
         {

+ 104 - 39
src/Avalonia.Styling/Styling/Styles.cs

@@ -27,40 +27,7 @@ namespace Avalonia.Styling
         public Styles()
         {
             _styles.ResetBehavior = ResetBehavior.Remove;
-            _styles.ForEachItem(
-                x =>
-                {
-                    if (x.ResourceParent == null && x is ISetResourceParent setParent)
-                    {
-                        setParent.SetParent(this);
-                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
-                    }
-
-                    if (x.HasResources)
-                    {
-                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
-                    }
-
-                    x.ResourcesChanged += NotifyResourcesChanged;
-                    _cache = null;
-                },
-                x =>
-                {
-                    if (x.ResourceParent == this && x is ISetResourceParent setParent)
-                    {
-                        setParent.SetParent(null);
-                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
-                    }
-
-                    if (x.HasResources)
-                    {
-                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
-                    }
-
-                    x.ResourcesChanged -= NotifyResourcesChanged;
-                    _cache = null;
-                },
-                () => { });
+            _styles.CollectionChanged += OnCollectionChanged;
         }
 
         public Styles(IResourceNode parent)
@@ -69,11 +36,7 @@ namespace Avalonia.Styling
             _parent = parent;
         }
 
-        public event NotifyCollectionChangedEventHandler CollectionChanged
-        {
-            add => _styles.CollectionChanged += value;
-            remove => _styles.CollectionChanged -= value;
-        }
+        public event NotifyCollectionChangedEventHandler? CollectionChanged;
 
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
@@ -121,6 +84,8 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
 
+        IReadOnlyList<IStyle> IStyle.Children => this;
+
         /// <inheritdoc/>
         public IStyle this[int index]
         {
@@ -257,6 +222,106 @@ namespace Avalonia.Styling
             NotifyResourcesChanged(e);
         }
 
+        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            static IReadOnlyList<T> ToReadOnlyList<T>(IList list)
+            {
+                if (list is IReadOnlyList<T>)
+                {
+                    return (IReadOnlyList<T>)list;
+                }
+                else
+                {
+                    var result = new T[list.Count];
+                    list.CopyTo(result, 0);
+                    return result;
+                }
+            }
+
+            void Add(IList items)
+            {
+                for (var i = 0; i < items.Count; ++i)
+                {
+                    var style = (IStyle)items[i];
+
+                    if (style.ResourceParent == null && style is ISetResourceParent setParent)
+                    {
+                        setParent.SetParent(this);
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                    }
+
+                    if (style.HasResources)
+                    {
+                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+                    }
+
+                    style.ResourcesChanged += NotifyResourcesChanged;
+                    _cache = null;
+                }
+
+                GetHost()?.StylesAdded(ToReadOnlyList<IStyle>(items));
+            }
+
+            void Remove(IList items)
+            {
+                for (var i = 0; i < items.Count; ++i)
+                {
+                    var style = (IStyle)items[i];
+
+                    if (style.ResourceParent == this && style is ISetResourceParent setParent)
+                    {
+                        setParent.SetParent(null);
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                    }
+
+                    if (style.HasResources)
+                    {
+                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+                    }
+
+                    style.ResourcesChanged -= NotifyResourcesChanged;
+                    _cache = null;
+                }
+
+                GetHost()?.StylesRemoved(ToReadOnlyList<IStyle>(items));
+            }
+
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+                    Add(e.NewItems);
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+                    Remove(e.OldItems);
+                    break;
+                case NotifyCollectionChangedAction.Replace:
+                    Remove(e.OldItems);
+                    Add(e.NewItems);
+                    break;
+                case NotifyCollectionChangedAction.Reset:
+                    throw new InvalidOperationException("Reset should not be called on Styles.");
+            }
+
+            CollectionChanged?.Invoke(this, e);
+        }
+
+        private IStyleHost? GetHost()
+        {
+            var node = _parent;
+
+            while (node != null)
+            {
+                if (node is IStyleHost host)
+                {
+                    return host;
+                }
+
+                node = node.ResourceParent;
+            }
+
+            return null;
+        }
+
         private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
             NotifyResourcesChanged(e);

+ 27 - 8
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -1,6 +1,10 @@
 <Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
         xmlns:sys="clr-namespace:System;assembly=netstandard">
-  
+  <Styles.Resources>
+    <conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter"/>
+  </Styles.Resources>
   <Style Selector="MenuItem">
     <Setter Property="Background" Value="Transparent"/>
     <Setter Property="BorderThickness" Value="1"/>
@@ -11,7 +15,14 @@
                 Background="{TemplateBinding Background}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}">
-          <Grid ColumnDefinitions="20,5,*,20">
+          <Grid>
+            <Grid.ColumnDefinitions>
+              <ColumnDefinition Width="20"/>
+              <ColumnDefinition Width="5"/>
+              <ColumnDefinition Width="*"/>
+              <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGT"/>
+              <ColumnDefinition Width="20"/>
+            </Grid.ColumnDefinitions>
             <ContentPresenter Name="icon"
                               Content="{TemplateBinding Icon}"
                               Width="16"
@@ -36,12 +47,16 @@
                 </DataTemplate>
               </ContentPresenter.DataTemplates>
             </ContentPresenter>
+            <TextBlock x:Name="PART_InputGestureText"
+                       Grid.Column="3"
+                       Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
+                       VerticalAlignment="Center"/>
             <Path Name="rightArrow"
                   Data="M0,0L4,3.5 0,7z"
                   Fill="{DynamicResource ThemeForegroundBrush}"
                   Margin="10,0,0,0"
                   VerticalAlignment="Center"
-                  Grid.Column="3"/>
+                  Grid.Column="4"/>
             <Popup Name="PART_Popup"
                    PlacementMode="Right"
                    StaysOpen="True"
@@ -54,7 +69,7 @@
                                     Items="{TemplateBinding Items}"
                                     ItemsPanel="{TemplateBinding ItemsPanel}"
                                     ItemTemplate="{TemplateBinding ItemTemplate}"
-                                    Margin="4 2"/>                                      
+                                    Grid.IsSharedSizeScope="True"/>
                 </ScrollViewer>
               </Border>
             </Popup>
@@ -100,10 +115,10 @@
                       BorderThickness="{TemplateBinding BorderThickness}">
                 <ScrollViewer>
                   <ItemsPresenter Name="PART_ItemsPresenter"
-                                    Items="{TemplateBinding Items}"
-                                    ItemsPanel="{TemplateBinding ItemsPanel}"
-                                    ItemTemplate="{TemplateBinding ItemTemplate}"
-                                    Margin="2"/>
+                                  Items="{TemplateBinding Items}"
+                                  ItemsPanel="{TemplateBinding ItemsPanel}"
+                                  ItemTemplate="{TemplateBinding ItemTemplate}"
+                                  Grid.IsSharedSizeScope="True"/>
                 </ScrollViewer>
               </Border>
             </Popup>
@@ -113,6 +128,10 @@
     </Setter>
   </Style>
 
+  <Style Selector="MenuItem /template/ ItemsPresenter#PART_ItemsPresenter">
+    <Setter Property="Margin" Value="2"/>
+  </Style>
+
   <Style Selector="MenuItem:selected /template/ Border#root">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

+ 1 - 0
src/Avalonia.Themes.Default/NativeMenuBar.xaml

@@ -13,6 +13,7 @@
         <Menu.Styles>
           <Style Selector="MenuItem">
             <Setter Property="Header" Value="{Binding Header}"/>
+            <Setter Property="InputGesture" Value="{Binding Gesture}"/>
             <Setter Property="Items" Value="{Binding Menu.Items}"/>
             <Setter Property="Command" Value="{Binding Command}"/>
             <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>

+ 96 - 0
src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs

@@ -0,0 +1,96 @@
+using System;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media.Imaging
+{
+    /// <summary>
+    /// Crops a Bitmap.
+    /// </summary>
+    public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable
+    {
+        /// <summary>
+        /// Defines the <see cref="Source"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IImage> SourceProperty =
+            AvaloniaProperty.Register<CroppedBitmap, IImage>(nameof(Source));
+
+        /// <summary>
+        /// Defines the <see cref="SourceRect"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PixelRect> SourceRectProperty =
+            AvaloniaProperty.Register<CroppedBitmap, PixelRect>(nameof(SourceRect));
+
+        public event EventHandler Invalidated;
+
+        static CroppedBitmap()
+        {
+            SourceRectProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceRectChanged(e));
+            SourceProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceChanged(e));
+        }
+
+        /// <summary>
+        /// Gets or sets the source for the bitmap.
+        /// </summary>
+        public IImage Source
+        {
+            get => GetValue(SourceProperty);
+            set => SetValue(SourceProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the rectangular area that the bitmap is cropped to.
+        /// </summary>
+        public PixelRect SourceRect
+        {
+            get => GetValue(SourceRectProperty);
+            set => SetValue(SourceRectProperty, value);
+        }
+
+        public CroppedBitmap()
+        {
+            Source = null;
+            SourceRect = default;
+        }
+
+        public CroppedBitmap(IImage source, PixelRect sourceRect)
+        {
+            Source = source;
+            SourceRect = sourceRect;
+        }
+
+        private void SourceChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.NewValue == null)
+                return;
+            if (!(e.NewValue is IBitmap))
+                throw new ArgumentException("Only IBitmap supported as source");
+            Invalidated?.Invoke(this, e);
+        }
+
+        private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e);
+
+        public virtual void Dispose()
+        {
+            (Source as IBitmap)?.Dispose();
+        }
+
+        public Size Size {
+            get
+            {
+                if (Source == null)
+                    return Size.Empty;
+                if (SourceRect.IsEmpty)
+                    return Source.Size;
+                return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi);
+            }
+        }
+
+        public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
+        {
+            if (Source == null)
+                return;
+            var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi);
+            Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode);           
+        }
+    }
+}

+ 18 - 9
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting
             var availableWidth = paragraphWidth;
             var currentWidth = 0.0;
             var runIndex = 0;
+            var length = 0;
 
             while (runIndex < textRuns.Count)
             {
                 var currentRun = textRuns[runIndex];
 
-                currentWidth += currentRun.GlyphRun.Bounds.Width;
-
-                if (currentWidth > availableWidth)
+                if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
                 {
-                    var measuredLength = MeasureText(currentRun, paragraphWidth);
+                    var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
 
-                    if (measuredLength < text.End)
+                    if (measuredLength < currentRun.Text.Length)
                     {
                         var currentBreakPosition = -1;
 
@@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    var splitResult = SplitTextRuns(textRuns, measuredLength);
+                    length += measuredLength;
+
+                    var splitResult = SplitTextRuns(textRuns, length);
 
                     var textLineMetrics =
                         TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
 
-                    return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
+                    return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics);
                 }
 
-                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                length += currentRun.GlyphRun.Characters.Length;
 
                 runIndex++;
             }
@@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting
 
                 if (measuredWidth + advance > availableWidth)
                 {
+                    index--;
                     break;
                 }
 
                 measuredWidth += advance;
             }
 
+            if(index < 0)
+            {
+                return 0;
+            }
+
             var cluster = textRun.GlyphRun.GlyphClusters[index];
 
             var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
@@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
+                var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
 
                 var first = new ShapedTextRun[firstCount];
 

+ 12 - 13
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -237,6 +237,12 @@ namespace Avalonia.Media.TextFormatting
 
                         textLines.Add(textLine);
 
+                        if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            currentPosition = _text.Length;
+                            break;
+                        }
+
                         if (_paragraphProperties.TextTrimming != TextTrimming.None)
                         {
                             currentPosition += remainingLength;
@@ -248,22 +254,15 @@ namespace Avalonia.Media.TextFormatting
 
                         currentPosition += textLine.Text.Length;
                     }
+                }
 
-                    if (lineBreaker.Current.Required && currentPosition == _text.Length)
-                    {
-                        var emptyTextLine = CreateEmptyTextLine(currentPosition);
-
-                        UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
-
-                        textLines.Add(emptyTextLine);
+                if (lineBreaker.Current.Required && currentPosition == _text.Length)
+                {
+                    var emptyTextLine = CreateEmptyTextLine(currentPosition);
 
-                        break;
-                    }
+                    UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
 
-                    if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height)
-                    {
-                        break;
-                    }
+                    textLines.Add(emptyTextLine);
                 }
 
                 Bounds = new Rect(left, 0, right, bottom);

+ 1 - 0
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -8,6 +8,7 @@ using Avalonia.Metadata;
 [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]

+ 8 - 2
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -355,15 +355,21 @@ namespace Avalonia.Rendering
 
                     node.BeginRender(context, isLayerRoot);
 
-                    foreach (var operation in node.DrawOperations)
+                    var drawOperations = node.DrawOperations;
+                    var drawOperationsCount = drawOperations.Count;
+                    for (int i = 0; i < drawOperationsCount; i++)
                     {
+                        var operation = drawOperations[i];
                         _currentDraw = operation;
                         operation.Item.Render(context);
                         _currentDraw = null;
                     }
 
-                    foreach (var child in node.Children)
+                    var children = node.Children;
+                    var childrenCount = children.Count;
+                    for (int i = 0; i < childrenCount; i++)
                     {
+                        var child = children[i];
                         Render(context, (VisualNode)child, layer, clipBounds);
                     }
 

+ 11 - 5
src/Avalonia.X11/X11Window.cs

@@ -173,6 +173,7 @@ namespace Avalonia.X11
             
             Surfaces = surfaces.ToArray();
             UpdateMotifHints();
+            UpdateSizeHints(null);
             _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
                 XNames.XNClientWindow, _handle, IntPtr.Zero);
             XFlush(_x11.Display);
@@ -219,12 +220,16 @@ namespace Avalonia.X11
             var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
                               MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
 
-            if (_popup || !_systemDecorations)
+            if (_popup || _systemDecorations == SystemDecorations.None)
             {
                 decorations = 0;
             }
+            else if (_systemDecorations == SystemDecorations.BorderOnly)
+            {
+                decorations = MotifDecorations.Border;
+            }
 
-            if (!_canResize)
+            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
             {
                 functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
                 decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@@ -247,7 +252,7 @@ namespace Avalonia.X11
             var min = _minMaxSize.minSize;
             var max = _minMaxSize.maxSize;
 
-            if (!_canResize)
+            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
                 max = min = _realSize;
             
             if (preResize.HasValue)
@@ -621,7 +626,7 @@ namespace Avalonia.X11
             return rv;
         }
         
-        private bool _systemDecorations = true;
+        private SystemDecorations _systemDecorations = SystemDecorations.Full;
         private bool _canResize = true;
         private const int MaxWindowDimension = 100000;
 
@@ -777,10 +782,11 @@ namespace Avalonia.X11
             (int)(point.X * Scaling + Position.X),
             (int)(point.Y * Scaling + Position.Y));
         
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
             _systemDecorations = enabled;
             UpdateMotifHints();
+            UpdateSizeHints(null);
         }
 
 

+ 14 - 9
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -6,6 +6,8 @@ using System;
 using Avalonia.Controls;
 using System.Collections.Generic;
 
+#nullable enable
+
 namespace Avalonia.Markup.Xaml.Styling
 {
     /// <summary>
@@ -14,8 +16,8 @@ namespace Avalonia.Markup.Xaml.Styling
     public class StyleInclude : IStyle, ISetResourceParent
     {
         private Uri _baseUri;
-        private IStyle _loaded;
-        private IResourceNode _parent;
+        private IStyle[]? _loaded;
+        private IResourceNode? _parent;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StyleInclude"/> class.
@@ -41,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Styling
         /// <summary>
         /// Gets or sets the source URL.
         /// </summary>
-        public Uri Source { get; set; }
+        public Uri? Source { get; set; }
 
         /// <summary>
         /// Gets the loaded style.
@@ -53,11 +55,12 @@ namespace Avalonia.Markup.Xaml.Styling
                 if (_loaded == null)
                 {
                     var loader = new AvaloniaXamlLoader();
-                    _loaded = (IStyle)loader.Load(Source, _baseUri);
-                    (_loaded as ISetResourceParent)?.SetParent(this);
+                    var loaded = (IStyle)loader.Load(Source, _baseUri);
+                    (loaded as ISetResourceParent)?.SetParent(this);
+                    _loaded = new[] { loaded };
                 }
 
-                return _loaded;
+                return _loaded?[0]!;
             }
         }
 
@@ -65,13 +68,15 @@ namespace Avalonia.Markup.Xaml.Styling
         bool IResourceProvider.HasResources => Loaded.HasResources;
 
         /// <inheritdoc/>
-        IResourceNode IResourceNode.ResourceParent => _parent;
+        IResourceNode? IResourceNode.ResourceParent => _parent;
+
+        IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
 
         /// <inheritdoc/>
-        public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host);
+        public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
 
         /// <inheritdoc/>
-        public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);
+        public bool TryGetResource(object key, out object? value) => Loaded.TryGetResource(key, out value);
 
         /// <inheritdoc/>
         void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)

+ 10 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop
         [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)]
         internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect);
 
+        [DllImport("dwmapi.dll")]
+        public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
 
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct MARGINS
+        {
+            public int cxLeftWidth;
+            public int cxRightWidth;
+            public int cyTopHeight;
+            public int cyBottomHeight;
+        }
 
         public enum MONITOR
         {

+ 12 - 8
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -34,7 +34,7 @@ namespace Avalonia.Win32
         private IInputRoot _owner;
         private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
         private bool _trackingMouse;
-        private bool _decorated = true;
+        private SystemDecorations _decorated = SystemDecorations.Full;
         private bool _resizable = true;
         private bool _topmost = false;
         private bool _taskbarIcon = true;
@@ -97,7 +97,7 @@ namespace Avalonia.Win32
         {
             get
             {
-                if (_decorated)
+                if (_decorated == SystemDecorations.Full)
                 {
                     var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE);
                     var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE);
@@ -281,7 +281,7 @@ namespace Avalonia.Win32
             UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide);
         }
 
-        public void SetSystemDecorations(bool value)
+        public void SetSystemDecorations(SystemDecorations value)
         {
             if (value == _decorated)
             {
@@ -464,7 +464,7 @@ namespace Avalonia.Win32
                     return IntPtr.Zero;
 
                 case WindowsMessage.WM_NCCALCSIZE:
-                    if (ToInt32(wParam) == 1 && !_decorated)
+                    if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full)
                     {
                         return IntPtr.Zero;
                     }
@@ -682,14 +682,14 @@ namespace Avalonia.Win32
                     
                     break;
                 case WindowsMessage.WM_NCPAINT:
-                    if (!_decorated)
+                    if (_decorated != SystemDecorations.Full)
                     {
                         return IntPtr.Zero;
                     }
                     break;
 
                 case WindowsMessage.WM_NCACTIVATE:
-                    if (!_decorated)
+                    if (_decorated != SystemDecorations.Full)
                     {
                         return new IntPtr(1);
                     }
@@ -1001,7 +1001,7 @@ namespace Avalonia.Win32
 
             style |= WindowStyles.WS_OVERLAPPEDWINDOW;
 
-            if (!_decorated)
+            if (_decorated != SystemDecorations.Full)
             {
                 style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU);
             }
@@ -1011,6 +1011,10 @@ namespace Avalonia.Win32
                 style ^= (WindowStyles.WS_SIZEFRAME);
             }
 
+            MARGINS margins = new MARGINS();
+            margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0;
+            UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins);
+
             GetClientRect(_hwnd, out var oldClientRect);
             var oldClientRectOrigin = new UnmanagedMethods.POINT();
             ClientToScreen(_hwnd, ref oldClientRectOrigin);
@@ -1024,7 +1028,7 @@ namespace Avalonia.Win32
             if (oldDecorated != _decorated)
             {
                 var newRect = oldClientRect;
-                if (_decorated)
+                if (_decorated == SystemDecorations.Full)
                     AdjustWindowRectEx(ref newRect, (uint)style, false,
                         GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE));
                 SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,

+ 1 - 1
src/iOS/Avalonia.iOS/EmbeddableImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.iOS
             return Disposable.Empty;
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 11 - 0
tests/Avalonia.Benchmarks/NullGlyphRun.cs

@@ -0,0 +1,11 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Benchmarks
+{
+    internal class NullGlyphRun : IGlyphRunImpl
+    {
+        public void Dispose()
+        {
+        }
+    }
+}

+ 3 - 1
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -72,7 +72,9 @@ namespace Avalonia.Benchmarks
 
         public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
         {
-            throw new NotImplementedException();
+            width = default;
+
+            return new NullGlyphRun();
         }
     }
 }

+ 43 - 0
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -385,6 +385,49 @@ namespace Avalonia.Controls.UnitTests
                 Assert.True(target.SelectionEnd <= "123".Length);
             }
         }
+
+        [Fact]
+        public void SelectedText_Changes_OnSelectionChange()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var target = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "0123456789"
+                };
+
+                Assert.True(target.SelectedText == "");
+
+                target.SelectionStart = 2;
+                target.SelectionEnd = 4;
+
+                Assert.True(target.SelectedText == "23");
+            }
+        }
+
+        [Fact]
+        public void SelectedText_EditsText()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var target = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "0123"
+                };
+
+                target.SelectedText = "AA";
+                Assert.True(target.Text == "AA0123");
+
+                target.SelectionStart = 1;
+                target.SelectionEnd = 3;
+                target.SelectedText = "BB";
+
+                Assert.True(target.Text == "ABB123");
+            }
+        }
+
         [Fact]
         public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
         {

+ 39 - 0
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@@ -9,6 +9,7 @@ using Avalonia.Input.Raw;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Platform;
+using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -269,6 +270,44 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Reacts_To_Changes_In_Global_Styles()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var impl = new Mock<ITopLevelImpl>();
+                impl.SetupGet(x => x.Scaling).Returns(1);
+
+                var child = new Border { Classes = { "foo" } };
+                var target = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTemplate(),
+                    Content = child,
+                };
+
+                target.LayoutManager.ExecuteInitialLayoutPass(target);
+
+                Assert.Equal(new Thickness(0), child.BorderThickness);
+
+                var style = new Style(x => x.OfType<Border>().Class("foo"))
+                {
+                    Setters =
+                    {
+                        new Setter(Border.BorderThicknessProperty, new Thickness(2))
+                    }
+                };
+
+                Application.Current.Styles.Add(style);
+                target.LayoutManager.ExecuteInitialLayoutPass(target);
+
+                Assert.Equal(new Thickness(2), child.BorderThickness);
+
+                Application.Current.Styles.Remove(style);
+
+                Assert.Equal(new Thickness(0), child.BorderThickness);
+            }
+        }
+
         private FuncControlTemplate<TestTopLevel> CreateTemplate()
         {
             return new FuncControlTemplate<TestTopLevel>((x, scope) =>

+ 25 - 6
tests/Avalonia.Input.UnitTests/KeyGestureTests.cs

@@ -1,19 +1,31 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using Xunit;
 
 namespace Avalonia.Input.UnitTests
 {
     public class KeyGestureTests
     {
-        public static readonly IEnumerable<object[]> SampleData = new object[][]
+        public static readonly IEnumerable<object[]> ParseData = new object[][]
         {
-            new object[]{"Ctrl+A", new KeyGesture(Key.A, InputModifiers.Control)},
-            new object[]{"  \tShift\t+Alt +B", new KeyGesture(Key.B, InputModifiers.Shift | InputModifiers.Alt) },
-            new object[]{"Control++", new KeyGesture(Key.OemPlus, InputModifiers.Control) }
+            new object[]{"Ctrl+A", new KeyGesture(Key.A, KeyModifiers.Control)},
+            new object[]{"  \tShift\t+Alt +B", new KeyGesture(Key.B, KeyModifiers.Shift | KeyModifiers.Alt) },
+            new object[]{"Control++", new KeyGesture(Key.OemPlus, KeyModifiers.Control) },
+            new object[]{ "Shift+⌘+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
+            new object[]{ "Shift+Cmd+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
+        };
+
+        public static readonly IEnumerable<object[]> ToStringData = new object[][]
+        {
+            new object[]{new KeyGesture(Key.A), "A"},
+            new object[]{new KeyGesture(Key.A, KeyModifiers.Control), "Ctrl+A"},
+            new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift), "Ctrl+Shift+A"},
+            new object[]{new KeyGesture(Key.A, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt+A"},
+            new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift), "Ctrl+Shift+Alt+A"},
+            new object[]{new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift), "Shift+Cmd+A"},
         };
 
         [Theory]
-        [MemberData(nameof(SampleData))]
+        [MemberData(nameof(ParseData))]
         public void Key_Gesture_Is_Able_To_Parse_Sample_Data(string text, KeyGesture gesture)
         {
             Assert.Equal(gesture, KeyGesture.Parse(text));
@@ -32,5 +44,12 @@ namespace Avalonia.Input.UnitTests
                 Key = pressedKey
             }));
         }
+
+        [Theory]
+        [MemberData(nameof(ToStringData))]
+        public void ToString_Produces_Correct_Results(KeyGesture gesture, string expected)
+        {
+            Assert.Equal(expected, gesture.ToString());
+        }
     }
 }

+ 24 - 1
tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using Avalonia.Interactivity;
 using Avalonia.VisualTree;
 using Xunit;
 
@@ -358,6 +357,29 @@ namespace Avalonia.Interactivity.UnitTests
             Assert.Equal(1, called);
         }
 
+        [Fact]
+        public void Removing_Control_In_Handler_Should_Not_Stop_Event()
+        {
+            // Issue #3176
+            var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive));
+            var invoked = new List<string>();
+            EventHandler<RoutedEventArgs> handler = (s, e) => invoked.Add(((TestInteractive)s).Name);
+            var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel);
+            var target = (IInteractive)parent.GetVisualChildren().Single();
+
+            EventHandler<RoutedEventArgs> removeHandler = (s, e) =>
+            {
+                parent.Children = Array.Empty<IVisual>();
+            };
+            
+            target.AddHandler(ev, removeHandler);
+
+            var args = new RoutedEventArgs(ev, target);
+            target.RaiseEvent(args);
+
+            Assert.Equal(new[] { "3", "2b", "1" }, invoked);
+        }
+
         private TestInteractive CreateTree(
             RoutedEvent ev,
             EventHandler<RoutedEventArgs> handler,
@@ -414,6 +436,7 @@ namespace Avalonia.Interactivity.UnitTests
 
                 set
                 {
+                    VisualChildren.Clear();
                     VisualChildren.AddRange(value.Cast<Visual>());
                 }
             }

+ 11 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs

@@ -9,6 +9,7 @@ using Avalonia.Styling;
 using Xunit;
 using System.ComponentModel;
 using Avalonia.Markup.Xaml.XamlIl.Runtime;
+using System.Collections.Generic;
 
 namespace Avalonia.Markup.Xaml.UnitTests.Converters
 {
@@ -144,6 +145,16 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters
                 throw new NotImplementedException();
             }
 
+            public void DetachStyles(IReadOnlyList<IStyle> styles)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void InvalidateStyles()
+            {
+                throw new NotImplementedException();
+            }
+
             public void StyleApplied(IStyleInstance instance)
             {
                 throw new NotImplementedException();

+ 38 - 5
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@@ -406,9 +406,13 @@ namespace Avalonia.Skia.UnitTests
         }
 
         [Theory]
-        [InlineData("abcde\r\n")]
-        [InlineData("abcde\n\r")]
-        public void Should_Break_With_BreakChar_Pair(string text)
+        [InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed
+        [InlineData("abcde\n\r", 7)] // This isn't valid but we somehow have to support it.
+        [InlineData("abcde\u000A", 6)] // Line Feed
+        [InlineData("abcde\u000B", 6)] // Vertical Tab
+        [InlineData("abcde\u000C", 6)] // Form Feed
+        [InlineData("abcde\u000D", 6)] // Carriage Return
+        public void Should_Break_With_BreakChar(string text, int expectedLength)
         {
             using (Start())
             {
@@ -422,11 +426,14 @@ namespace Avalonia.Skia.UnitTests
 
                 Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
 
-                Assert.Equal(7, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
+                Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
 
                 Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
 
-                Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
+                if(expectedLength == 7)
+                {
+                    Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
+                }
             }
         }
 
@@ -473,6 +480,32 @@ namespace Avalonia.Skia.UnitTests
             }
         }
 
+        [InlineData("0123456789\r0123456789", 2)]
+        [InlineData("0123456789", 1)]
+        [Theory]
+        public void Should_Include_Last_Line_When_Constraint_Is_Surpassed(string text, int numberOfLines)
+        {
+            using (Start())
+            {
+                var glyphTypeface = Typeface.Default.GlyphTypeface;
+
+                var emHeight = glyphTypeface.DesignEmHeight;
+
+                var lineHeight = (glyphTypeface.Descent - glyphTypeface.Ascent) * (12.0 / emHeight);
+
+                var layout = new TextLayout(
+                    text,
+                    Typeface.Default,
+                    12,
+                    Brushes.Black.ToImmutable(),
+                    maxHeight: lineHeight * numberOfLines - lineHeight * 0.5);
+
+                Assert.Equal(numberOfLines, layout.TextLines.Count);
+
+                Assert.Equal(numberOfLines * lineHeight, layout.Bounds.Height);
+            }
+        }
+
         public static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

+ 179 - 0
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@@ -244,6 +244,185 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(new Thickness(0), border.BorderThickness);
         }
 
+        [Fact]
+        public void Removing_Style_Should_Detach_From_Control()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var border = new Border();
+                var root = new TestRoot
+                {
+                    Styles =
+                    { 
+                        new Style(x => x.OfType<Border>())
+                        {
+                            Setters =
+                            {
+                                new Setter(Border.BorderThicknessProperty, new Thickness(4)),
+                            }
+                        }
+                    },
+                    Child = border,
+                };
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(4), border.BorderThickness);
+
+                root.Styles.RemoveAt(0);
+                Assert.Equal(new Thickness(0), border.BorderThickness);
+            }
+        }
+
+        [Fact]
+        public void Adding_Style_Should_Attach_To_Control()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var border = new Border();
+                var root = new TestRoot
+                {
+                    Styles =
+                    {
+                        new Style(x => x.OfType<Border>())
+                        {
+                            Setters =
+                            {
+                                new Setter(Border.BorderThicknessProperty, new Thickness(4)),
+                            }
+                        }
+                    },
+                    Child = border,
+                };
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(4), border.BorderThickness);
+
+                root.Styles.Add(new Style(x => x.OfType<Border>())
+                {
+                    Setters =
+                    {
+                        new Setter(Border.BorderThicknessProperty, new Thickness(6)),
+                    }
+                });
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(6), border.BorderThickness);
+            }
+        }
+
+        [Fact]
+        public void Removing_Style_With_Nested_Style_Should_Detach_From_Control()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var border = new Border();
+                var root = new TestRoot
+                {
+                    Styles =
+                    {
+                        new Styles
+                        {
+                            new Style(x => x.OfType<Border>())
+                            {
+                                Setters =
+                                {
+                                    new Setter(Border.BorderThicknessProperty, new Thickness(4)),
+                                }
+                            }
+                        }
+                    },
+                    Child = border,
+                };
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(4), border.BorderThickness);
+
+                root.Styles.RemoveAt(0);
+                Assert.Equal(new Thickness(0), border.BorderThickness);
+            }
+        }
+        
+        [Fact]
+        public void Adding_Nested_Style_Should_Attach_To_Control()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var border = new Border();
+                var root = new TestRoot
+                {
+                    Styles =
+                    {
+                        new Styles
+                        {
+                            new Style(x => x.OfType<Border>())
+                            {
+                                Setters =
+                                {
+                                    new Setter(Border.BorderThicknessProperty, new Thickness(4)),
+                                }
+                            }
+                        }
+                    },
+                    Child = border,
+                };
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(4), border.BorderThickness);
+
+                ((Styles)root.Styles[0]).Add(new Style(x => x.OfType<Border>())
+                {
+                    Setters =
+                    {
+                        new Setter(Border.BorderThicknessProperty, new Thickness(6)),
+                    }
+                });
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(6), border.BorderThickness);
+            }
+        }
+
+        [Fact]
+        public void Removing_Nested_Style_Should_Detach_From_Control()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var border = new Border();
+                var root = new TestRoot
+                {
+                    Styles =
+                    {
+                        new Styles
+                        {
+                            new Style(x => x.OfType<Border>())
+                            {
+                                Setters =
+                                {
+                                    new Setter(Border.BorderThicknessProperty, new Thickness(4)),
+                                }
+                            },
+                            new Style(x => x.OfType<Border>())
+                            {
+                                Setters =
+                                {
+                                    new Setter(Border.BorderThicknessProperty, new Thickness(6)),
+                                }
+                            },
+                        }
+                    },
+                    Child = border,
+                };
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(6), border.BorderThickness);
+
+                ((Styles)root.Styles[0]).RemoveAt(1);
+
+                root.Measure(Size.Infinity);
+                Assert.Equal(new Thickness(4), border.BorderThickness);
+            }
+        }
+
         private class Class1 : Control
         {
             public static readonly StyledProperty<string> FooProperty =

+ 1 - 1
tests/Avalonia.UnitTests/MouseTestHelper.cs

@@ -56,7 +56,7 @@ namespace Avalonia.UnitTests
             {
                 _pressedButton = mouseButton;
                 _pointer.Capture((IInputElement)target);
-                target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
+                source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
                     GetModifiers(modifiers), clickCount));
             }
         }