Browse Source

Merge branch 'master' into fixes/5027-avaloniaobject-batching

Dariusz Komosiński 4 years ago
parent
commit
88bc11b9e5
100 changed files with 1385 additions and 607 deletions
  1. 4 3
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 8 0
      .github/ISSUE_TEMPLATE/config.yml
  3. 0 1
      .github/ISSUE_TEMPLATE/feature_request.md
  4. 22 0
      native/Avalonia.Native/src/OSX/cursor.mm
  5. 4 4
      native/Avalonia.Native/src/OSX/window.mm
  6. BIN
      samples/ControlCatalog/Assets/avalonia-32.png
  7. 4 0
      samples/ControlCatalog/MainView.xaml
  8. 4 4
      samples/ControlCatalog/Pages/AcrylicPage.xaml
  9. 29 0
      samples/ControlCatalog/Pages/CursorPage.xaml
  10. 20 0
      samples/ControlCatalog/Pages/CursorPage.xaml.cs
  11. 3 2
      samples/ControlCatalog/Pages/DataGridPage.xaml
  12. 4 2
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  13. 7 0
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  14. 15 0
      samples/ControlCatalog/Pages/SliderPage.xaml
  15. 44 0
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  16. 27 1
      src/Avalonia.Base/Data/BindingOperations.cs
  17. 26 4
      src/Avalonia.Base/EnumExtensions.cs
  18. 3 2
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  19. 2 2
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  20. 1 1
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  21. 13 8
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  22. 10 6
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  23. 3 2
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  24. 6 0
      src/Avalonia.Controls/ApiCompatBaseline.txt
  25. 13 0
      src/Avalonia.Controls/Button.cs
  26. 1 1
      src/Avalonia.Controls/ComboBox.cs
  27. 38 39
      src/Avalonia.Controls/ContextMenu.cs
  28. 1 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  29. 52 3
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  30. 16 29
      src/Avalonia.Controls/Grid.cs
  31. 4 4
      src/Avalonia.Controls/ListBox.cs
  32. 51 3
      src/Avalonia.Controls/MenuItem.cs
  33. 5 1
      src/Avalonia.Controls/NativeControlHost.cs
  34. 1 1
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  35. 6 6
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  36. 3 1
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  37. 10 11
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  38. 20 28
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  39. 4 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  40. 2 2
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  41. 38 9
      src/Avalonia.Controls/Slider.cs
  42. 2 2
      src/Avalonia.Controls/TextBox.cs
  43. 1 1
      src/Avalonia.Controls/TopLevel.cs
  44. 6 6
      src/Avalonia.Controls/TreeView.cs
  45. 2 0
      src/Avalonia.Controls/Utils/AncestorFinder.cs
  46. 1 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  47. 9 3
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  48. 1 1
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  49. 4 4
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  50. 2 2
      src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  51. 1 1
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  52. 5 3
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  53. 1 1
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  54. 1 1
      src/Avalonia.Input/AccessKeyHandler.cs
  55. 4 0
      src/Avalonia.Input/ApiCompatBaseline.txt
  56. 18 21
      src/Avalonia.Input/Cursor.cs
  57. 3 1
      src/Avalonia.Input/FocusManager.cs
  58. 12 0
      src/Avalonia.Input/Platform/ICursorFactory.cs
  59. 14 0
      src/Avalonia.Input/Platform/ICursorImpl.cs
  60. 0 9
      src/Avalonia.Input/Platform/IStandardCursorFactory.cs
  61. 1 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  62. 22 3
      src/Avalonia.Native/Cursor.cs
  63. 3 3
      src/Avalonia.Native/WindowImplBase.cs
  64. 1 0
      src/Avalonia.Native/avn.idl
  65. 1 1
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  66. 2 0
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  67. 6 6
      src/Avalonia.ReactiveUI/ReactiveUserControl.cs
  68. 7 7
      src/Avalonia.ReactiveUI/ReactiveWindow.cs
  69. 8 8
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  70. 8 8
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  71. 6 6
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  72. 4 1
      src/Avalonia.Themes.Default/ProgressBar.xaml
  73. 3 2
      src/Avalonia.Themes.Default/Slider.xaml
  74. 4 2
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  75. 6 6
      src/Avalonia.Themes.Fluent/Controls/Slider.xaml
  76. 2 2
      src/Avalonia.Visuals/Media/FormattedText.cs
  77. 18 0
      src/Avalonia.Visuals/Media/PathGeometryCollections.cs
  78. 0 56
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs
  79. 15 12
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
  80. 394 146
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  81. 74 0
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs
  82. 70 3
      src/Avalonia.X11/X11CursorFactory.cs
  83. 1 1
      src/Avalonia.X11/X11Platform.cs
  84. 1 1
      src/Avalonia.X11/X11Structs.cs
  85. 2 2
      src/Avalonia.X11/X11Window.Ime.cs
  86. 12 14
      src/Avalonia.X11/X11Window.cs
  87. 5 5
      src/Avalonia.X11/XI2Manager.cs
  88. 7 0
      src/Avalonia.X11/XLib.cs
  89. 1 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  90. 7 2
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  91. 1 1
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
  92. 6 3
      src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs
  93. 9 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  94. 2 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  95. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  96. 7 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
  97. 2 0
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  98. 26 23
      src/Markup/Avalonia.Markup/Data/Binding.cs
  99. 22 27
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  100. 7 7
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs

+ 4 - 3
.github/ISSUE_TEMPLATE/bug_report.md

@@ -4,7 +4,6 @@ about: Create a report to help us improve Avalonia
 title: ''
 labels: bug
 assignees: ''
-
 ---
 
 **Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
 
 **To Reproduce**
 Steps to reproduce the behavior:
+
 1. Go to '...'
 2. Click on '....'
 3. Scroll down to '....'
@@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
 If applicable, add screenshots to help explain your problem.
 
 **Desktop (please complete the following information):**
- - OS: [e.g. Windows, Mac, Linux (State distribution)]
- - Version [e.g. 0.10.0-rc1 or 0.9.12]
+
+- OS: [e.g. Windows, Mac, Linux (State distribution)]
+- Version [e.g. 0.10.0-rc1 or 0.9.12]
 
 **Additional context**
 Add any other context about the problem here.

+ 8 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Questions, Discussions, Ideas
+    url: https://github.com/AvaloniaUI/Avalonia/discussions/new
+    about: Please ask and answer questions here.
+  - name: Avalonia Community Support on Gitter
+    url: https://gitter.im/AvaloniaUI/Avalonia
+    about: Please ask and answer questions here.

+ 0 - 1
.github/ISSUE_TEMPLATE/feature_request.md

@@ -4,7 +4,6 @@ about: Suggest an idea for this project
 title: ''
 labels: enhancement
 assignees: ''
-
 ---
 
 **Is your feature request related to a problem? Please describe.**

+ 22 - 0
native/Avalonia.Native/src/OSX/cursor.mm

@@ -62,6 +62,28 @@ public:
             
         return S_OK;
     }
+    
+    virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override
+    {
+        if(bitmapData == nullptr || retOut == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        NSData *imageData = [NSData dataWithBytes:bitmapData length:length];
+        NSImage *image = [[NSImage alloc] initWithData:imageData];
+        
+        
+        NSPoint hotSpot;
+        hotSpot.x = hotPixel.Width;
+        hotSpot.y = hotPixel.Height;
+        
+        *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]);
+        
+        (*retOut)->AddRef();
+        
+        return S_OK;
+    }
 };
 
 extern IAvnCursorFactory* CreateCursorFactory()

+ 4 - 4
native/Avalonia.Native/src/OSX/window.mm

@@ -2068,17 +2068,17 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 -(void)becomeKeyWindow
 {
+    [self showWindowMenuWithAppMenu];
+    
     if([self activateAppropriateChild: true])
     {
-        [self showWindowMenuWithAppMenu];
-        
         if(_parent != nullptr)
         {
             _parent->BaseEvents->Activated();
         }
-        
-        [super becomeKeyWindow];
     }
+    
+    [super becomeKeyWindow];
 }
 
 -(void) restoreParentWindow;

BIN
samples/ControlCatalog/Assets/avalonia-32.png


+ 4 - 0
samples/ControlCatalog/MainView.xaml

@@ -22,6 +22,10 @@
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
       <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
+      <TabItem Header="Cursor"
+               ScrollViewer.VerticalScrollBarVisibility="Disabled">
+        <pages:CursorPage/>
+      </TabItem>
       <TabItem Header="DataGrid" 
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">

+ 4 - 4
samples/ControlCatalog/Pages/AcrylicPage.xaml

@@ -16,13 +16,13 @@
         <StackPanel Spacing="5" Margin="40 10">
           <StackPanel Orientation="Horizontal">
             <TextBlock Text="TintOpacity" Foreground="Black" />
-            <Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" Width="400" />
-            <TextBlock Text="{Binding #TintOpacitySlider.Value}" Foreground="Black" />
+            <Slider Name="TintOpacitySlider" Minimum="0" Maximum="1" Value="0.9" SmallChange="0.1" LargeChange="0.2" Width="400" />
+            <TextBlock Text="{Binding #TintOpacitySlider.Value, StringFormat=\{0:0.#\}}" Foreground="Black" />
           </StackPanel>
           <StackPanel Orientation="Horizontal">
             <TextBlock Text="MaterialOpacity" Foreground="Black" />
-            <Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" Width="400" />
-            <TextBlock Text="{Binding #MaterialOpacitySlider.Value}" Foreground="Black" />
+            <Slider Name="MaterialOpacitySlider" Minimum="0" Maximum="1" Value="0.8" SmallChange="0.1" LargeChange="0.2" Width="400" />
+            <TextBlock Text="{Binding #MaterialOpacitySlider.Value, StringFormat=\{0:0.#\}}" Foreground="Black" />
           </StackPanel>
         </StackPanel>
       </ExperimentalAcrylicBorder>

+ 29 - 0
samples/ControlCatalog/Pages/CursorPage.xaml

@@ -0,0 +1,29 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.CursorPage">
+  <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*">
+    <StackPanel Grid.ColumnSpan="2" Orientation="Vertical" Spacing="4">
+      <TextBlock Classes="h1">Cursor</TextBlock>
+      <TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
+    </StackPanel>
+
+    <ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
+      <ListBox.Styles>
+        <Style Selector="ListBoxItem">
+          <Setter Property="Cursor" Value="{Binding Cursor}"/>
+        </Style>
+      </ListBox.Styles>
+      <ListBox.ItemTemplate>
+        <DataTemplate>
+          <TextBlock Text="{Binding Type}"/>
+        </DataTemplate>
+      </ListBox.ItemTemplate>
+    </ListBox>
+
+    <StackPanel Grid.Column="1" Grid.Row="1" Margin="8 8 0 8">
+      <Button Cursor="{Binding CustomCursor}" Margin="0 8" Padding="16">
+        <TextBlock>Custom Cursor</TextBlock>
+      </Button>
+    </StackPanel>
+ </Grid>
+</UserControl>

+ 20 - 0
samples/ControlCatalog/Pages/CursorPage.xaml.cs

@@ -0,0 +1,20 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+    public class CursorPage : UserControl
+    {
+        public CursorPage()
+        {
+            this.InitializeComponent();
+            DataContext = new CursorPageViewModel();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 3 - 2
samples/ControlCatalog/Pages/DataGridPage.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:local="clr-namespace:ControlCatalog.Models;assembly=ControlCatalog"
+             xmlns:local="using:ControlCatalog.Models"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.DataGridPage">
   <UserControl.Resources>
@@ -26,7 +26,8 @@
         <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
           <DataGrid.Columns>
             <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
-            <DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" />
+            <!-- CompiledBinding example of usage. -->
+            <DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
             <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />
             <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" />
             <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" CellStyleClasses="gdp" />

+ 4 - 2
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@@ -24,8 +24,10 @@ namespace ControlCatalog.Pages
             dg1.LoadingRow += Dg1_LoadingRow;
             dg1.Sorting += (s, a) =>
             {
-                var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path;
-                if (property == dataGridSortDescription.PropertyPath
+                var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding;
+
+                if (binding?.Path is string property
+                    && property == dataGridSortDescription.PropertyPath
                     && !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
                 {
                     collectionView1.SortDescriptions.Add(dataGridSortDescription);

+ 7 - 0
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@@ -15,6 +15,13 @@
         <Slider Name="hprogress" Maximum="100" Value="40" />
         <Slider Name="vprogress" Maximum="100" Value="60" />
       </StackPanel>
+
+      <StackPanel Spacing="10">
+        <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
+        <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
+        <ProgressBar VerticalAlignment="Center" Value="50" />
+        <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
+      </StackPanel>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 15 - 0
samples/ControlCatalog/Pages/SliderPage.xaml

@@ -45,6 +45,12 @@
             <sys:Exception /> 
           </DataValidationErrors.Error>
         </Slider>
+        <Slider Value="0"
+                IsDirectionReversed="True"
+                Minimum="0"
+                Maximum="100"
+                TickFrequency="10"
+                Width="300" />
       </StackPanel>
       <Slider Value="0"
               Minimum="0"
@@ -54,6 +60,15 @@
               TickPlacement="Outside"
               TickFrequency="10"
               Height="300"/>
+      <Slider Value="0"
+              IsDirectionReversed="True"
+              Minimum="0"
+              Maximum="100"
+              Orientation="Vertical"
+              IsSnapToTickEnabled="True"
+              TickPlacement="Outside"
+              TickFrequency="10"
+              Height="300"/>
     </StackPanel>
 
   </StackPanel>

+ 44 - 0
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class CursorPageViewModel : ViewModelBase
+    {
+        public CursorPageViewModel()
+        {
+            StandardCursors = Enum.GetValues(typeof(StandardCursorType))
+                .Cast<StandardCursorType>()
+                .Select(x => new StandardCursorModel(x))
+                .ToList();
+
+            var loader = AvaloniaLocator.Current.GetService<IAssetLoader>();
+            var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
+            var bitmap = new Bitmap(s);
+            CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));
+        }
+
+        public IEnumerable<StandardCursorModel> StandardCursors { get; }
+        
+        public Cursor CustomCursor { get; }
+
+        public class StandardCursorModel
+        {
+            public StandardCursorModel(StandardCursorType type)
+            {
+                Type = type;
+                Cursor = new Cursor(type);
+            }
+
+            public StandardCursorType Type { get; }
+            
+            public Cursor Cursor { get; }
+        }
+    }
+}

+ 27 - 1
src/Avalonia.Base/Data/BindingOperations.cs

@@ -45,7 +45,7 @@ namespace Avalonia.Data
                 case BindingMode.OneWay:
                     return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority);
                 case BindingMode.TwoWay:
-                    return new CompositeDisposable(
+                    return new TwoWayBindingDisposable(
                         target.Bind(property, binding.Subject, binding.Priority),
                         target.GetObservable(property).Subscribe(binding.Subject));
                 case BindingMode.OneTime:
@@ -88,6 +88,32 @@ namespace Avalonia.Data
                     throw new ArgumentException("Invalid binding mode.");
             }
         }
+
+        private sealed class TwoWayBindingDisposable : IDisposable
+        {
+            private readonly IDisposable _first;
+            private readonly IDisposable _second;
+            private bool _isDisposed;
+
+            public TwoWayBindingDisposable(IDisposable first, IDisposable second)
+            {
+                _first = first;
+                _second = second;
+            }
+
+            public void Dispose()
+            {
+                if (_isDisposed)
+                {
+                    return;
+                }
+
+                _first.Dispose();
+                _second.Dispose();
+
+                _isDisposed = true;
+            }
+        }
     }
 
     public sealed class DoNothingType

+ 26 - 4
src/Avalonia.Base/EnumExtensions.cs

@@ -11,10 +11,32 @@ namespace Avalonia
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
         {
-            var intValue = *(int*)&value;
-            var intFlag = *(int*)&flag;
-
-            return (intValue & intFlag) == intFlag;
+            if (sizeof(T) == 1)
+            {
+                var byteValue = Unsafe.As<T, byte>(ref value);
+                var byteFlag = Unsafe.As<T, byte>(ref flag);
+                return (byteValue & byteFlag) == byteFlag;
+            }
+            else if (sizeof(T) == 2)
+            {
+                var shortValue = Unsafe.As<T, short>(ref value);
+                var shortFlag = Unsafe.As<T, short>(ref flag);
+                return (shortValue & shortFlag) == shortFlag;
+            }
+            else if (sizeof(T) == 4)
+            {
+                var intValue = Unsafe.As<T, int>(ref value);
+                var intFlag = Unsafe.As<T, int>(ref flag);
+                return (intValue & intFlag) == intFlag;
+            }
+            else if (sizeof(T) == 8)
+            {
+                var longValue = Unsafe.As<T, long>(ref value);
+                var longFlag = Unsafe.As<T, long>(ref flag);
+                return (longValue & longFlag) == longFlag;
+            }
+            else
+                throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");
         }
     }
 }

+ 3 - 2
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 
 namespace Avalonia.Reactive
 {
@@ -55,9 +56,9 @@ namespace Avalonia.Reactive
                     newValue = (T)e.Sender.GetValue(e.Property);
                 }
 
-                if (!Equals(newValue, _value))
+                if (!EqualityComparer<T>.Default.Equals(newValue, _value))
                 {
-                    _value = (T)newValue;
+                    _value = newValue;
                     PublishNext(_value);
                 }
             }

+ 2 - 2
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -372,8 +372,8 @@ namespace Avalonia.Utilities
             const string implicitName = "op_Implicit";
             const string explicitName = "op_Explicit";
 
-            bool allowImplicit = (operatorType & OperatorType.Implicit) != 0;
-            bool allowExplicit = (operatorType & OperatorType.Explicit) != 0;
+            bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit);
+            bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit);
 
             foreach (MethodInfo method in fromType.GetMethods())
             {

+ 1 - 1
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -2595,7 +2595,7 @@ namespace Avalonia.Collections
         /// <returns>Whether the specified flag is set</returns>
         private bool CheckFlag(CollectionViewFlags flags)
         {
-            return (_flags & flags) != 0;
+            return _flags.HasFlagCustom(flags);
         }
 
         /// <summary>

+ 13 - 8
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -10,7 +10,8 @@ using System.Reactive.Disposables;
 using System.Reactive.Subjects;
 using Avalonia.Reactive;
 using System.Diagnostics;
-using Avalonia.Controls.Utils; 
+using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
 
 namespace Avalonia.Controls
 {
@@ -47,14 +48,15 @@ namespace Avalonia.Controls
 
                     if (_binding != null)
                     {
-                        if(_binding is Avalonia.Data.Binding binding)
+                        if(_binding is BindingBase binding)
                         {
                             if (binding.Mode == BindingMode.OneWayToSource)
                             {
                                 throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
                             }
 
-                            if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
+                            var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+                            if (!string.IsNullOrEmpty(path) && binding.Mode == BindingMode.Default)
                             {
                                 binding.Mode = BindingMode.TwoWay;
                             } 
@@ -136,13 +138,16 @@ namespace Avalonia.Controls
         internal void SetHeaderFromBinding()
         {
             if (OwningGrid != null && OwningGrid.DataConnection.DataType != null
-                && Header == null && Binding != null && Binding is Binding binding
-                && !String.IsNullOrWhiteSpace(binding.Path))
+                && Header == null && Binding != null && Binding is BindingBase binding)
             {
-                string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path);
-                if (header != null)
+                var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+                if (!string.IsNullOrWhiteSpace(path))
                 {
-                    Header = header;
+                    var header = OwningGrid.DataConnection.DataType.GetDisplayName(path);
+                    if (header != null)
+                    {
+                        Header = header;
+                    }
                 }
             }
         }

+ 10 - 6
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -12,6 +12,7 @@ using System;
 using System.Linq;
 using System.Diagnostics;
 using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
 
 namespace Avalonia.Controls
 {
@@ -1033,13 +1034,16 @@ namespace Avalonia.Controls
 
             if (String.IsNullOrEmpty(result))
             {
-
-                if(this is DataGridBoundColumn boundColumn && 
-                    boundColumn.Binding != null &&
-                    boundColumn.Binding is Binding binding &&
-                    binding.Path != null)
+                if (this is DataGridBoundColumn boundColumn)
                 {
-                    result = binding.Path;
+                    if (boundColumn.Binding is Binding binding)
+                    {
+                        result = binding.Path;
+                    }
+                    else if (boundColumn.Binding is CompiledBindingExtension compiledBinding)
+                    {
+                        result = compiledBinding.Path.ToString();
+                    }
                 }
             }
 

+ 3 - 2
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@@ -5,6 +5,7 @@
 
 using Avalonia.Controls.Utils;
 using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
 using Avalonia.Utilities;
 using System;
 using System.Collections.Generic;
@@ -141,9 +142,9 @@ namespace Avalonia.Controls
             Debug.Assert(dataGridColumn != null);
 
             if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn && 
-                dataGridBoundColumn.Binding is Binding binding)
+                dataGridBoundColumn.Binding is BindingBase binding)
             {
-                string path = binding.Path;
+                var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
 
                 if (string.IsNullOrWhiteSpace(path))
                 {

+ 6 - 0
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -0,0 +1,6 @@
+Compat issues with assembly Avalonia.Controls:
+MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 4

+ 13 - 0
src/Avalonia.Controls/Button.cs

@@ -80,6 +80,7 @@ namespace Avalonia.Controls
 
         private ICommand _command;
         private bool _commandCanExecute = true;
+        private KeyGesture _hotkey;
 
         /// <summary>
         /// Initializes static members of the <see cref="Button"/> class.
@@ -207,6 +208,11 @@ namespace Avalonia.Controls
 
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
+            if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
+            {
+                HotKey = _hotkey;
+            }
+            
             base.OnAttachedToLogicalTree(e);
 
             if (Command != null)
@@ -217,6 +223,13 @@ namespace Avalonia.Controls
 
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
+            // This will cause the hotkey manager to dispose the observer and the reference to this control
+            if (HotKey != null)
+            {
+                _hotkey = HotKey;
+                HotKey = null;
+            }
+
             base.OnDetachedFromLogicalTree(e);
 
             if (Command != null)

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

@@ -188,7 +188,7 @@ namespace Avalonia.Controls
                 return;
 
             if (e.Key == Key.F4 ||
-                ((e.Key == Key.Down || e.Key == Key.Up) && ((e.KeyModifiers & KeyModifiers.Alt) != 0)))
+                ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)))
             {
                 IsDropDownOpen = !IsDropDownOpen;
                 e.Handled = true;

+ 38 - 39
src/Avalonia.Controls/ContextMenu.cs

@@ -269,7 +269,43 @@ namespace Avalonia.Controls
             }
 
             control ??= _attachedControls![0];
+            Open(control, PlacementTarget ?? control);
+        }
+
+        /// <summary>
+        /// Closes the menu.
+        /// </summary>
+        public override void Close()
+        {
+            if (!IsOpen)
+            {
+                return;
+            }
 
+            if (_popup != null && _popup.IsVisible)
+            {
+                _popup.IsOpen = false;
+            }
+        }
+
+        void ISetterValue.Initialize(ISetter setter)
+        {
+            // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
+            // the behavior defined in Control which requires controls to be wrapped in a <template>.
+            if (!(setter is Setter s && s.Property == ContextMenuProperty))
+            {
+                throw new InvalidOperationException(
+                    "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
+            }
+        }
+
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new MenuItemContainerGenerator(this);
+        }
+
+        private void Open(Control control, Control placementTarget)
+        {
             if (IsOpen)
             {
                 return;
@@ -286,7 +322,6 @@ namespace Avalonia.Controls
                     PlacementGravity = PlacementGravity,
                     PlacementMode = PlacementMode,
                     PlacementRect = PlacementRect,
-                    PlacementTarget = PlacementTarget ?? control,
                     IsLightDismissEnabled = true,
                     OverlayDismissEventPassThrough = true,
                     WindowManagerAddShadowHint = WindowManagerAddShadowHint,
@@ -302,11 +337,7 @@ namespace Avalonia.Controls
                 ((ISetLogicalParent)_popup).SetParent(control);
             }
 
-            if (PlacementTarget is null && _popup.PlacementTarget != control)
-            {
-                _popup.PlacementTarget = control;
-            }
-
+            _popup.PlacementTarget = placementTarget;
             _popup.Child = this;
             IsOpen = true;
             _popup.IsOpen = true;
@@ -318,38 +349,6 @@ namespace Avalonia.Controls
             });
         }
 
-        /// <summary>
-        /// Closes the menu.
-        /// </summary>
-        public override void Close()
-        {
-            if (!IsOpen)
-            {
-                return;
-            }
-
-            if (_popup != null && _popup.IsVisible)
-            {
-                _popup.IsOpen = false;
-            }
-        }
-
-        void ISetterValue.Initialize(ISetter setter)
-        {
-            // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
-            // the behavior defined in Control which requires controls to be wrapped in a <template>.
-            if (!(setter is Setter s && s.Property == ContextMenuProperty))
-            {
-                throw new InvalidOperationException(
-                    "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
-            }
-        }
-
-        protected override IItemContainerGenerator CreateItemContainerGenerator()
-        {
-            return new MenuItemContainerGenerator(this);
-        }
-
         private void PopupOpened(object sender, EventArgs e)
         {
             _previousFocus = FocusManager.Instance?.Current;
@@ -403,7 +402,7 @@ namespace Avalonia.Controls
                 if (contextMenu.CancelOpening())
                     return;
 
-                contextMenu.Open(control);
+                contextMenu.Open(control, e.Source as Control ?? control);
                 e.Handled = true;
             }
         }

+ 1 - 1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -61,7 +61,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
 
         public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1);
 
-        public virtual void SetCursor(IPlatformHandle cursor)
+        public virtual void SetCursor(ICursorImpl cursor)
         {
         }
 

+ 52 - 3
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@@ -1,4 +1,10 @@
+using System;
+using System.Collections.Generic;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+using Avalonia.Reactive;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Generators
 {    
@@ -16,11 +22,15 @@ namespace Avalonia.Controls.Generators
         {
             var tabItem = (TabItem)base.CreateContainer(item);
 
-            tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
+            tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
+                tabItem,
+                TabControl.TabStripPlacementProperty));
 
             if (tabItem.HeaderTemplate == null)
             {
-                tabItem[~HeaderedContentControl.HeaderTemplateProperty] = Owner[~ItemsControl.ItemTemplateProperty];
+                tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ItemTemplateProperty));
             }
 
             if (tabItem.Header == null)
@@ -40,10 +50,49 @@ namespace Avalonia.Controls.Generators
 
             if (!(tabItem.Content is IControl))
             {
-                tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
+                tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate>(
+                    tabItem,
+                    TabControl.ContentTemplateProperty));
             }
 
             return tabItem;
         }
+
+        private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
+        {
+            private readonly TabItem _item;
+            private readonly StyledProperty<T> _ownerProperty;
+            private IDisposable _ownerSubscription;
+            private IDisposable _propertySubscription;
+
+            public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
+            {
+                _item = item;
+                _ownerProperty = ownerProperty;
+            }
+
+            protected override void Subscribed()
+            {
+                _ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
+            }
+
+            protected override void Unsubscribed()
+            {
+                _ownerSubscription?.Dispose();
+                _ownerSubscription = null;
+            }
+
+            private void OwnerChanged(ILogical c)
+            {
+                _propertySubscription?.Dispose();
+                _propertySubscription = null;
+
+                if (c is TabControl tabControl)
+                {
+                    _propertySubscription = tabControl.GetObservable(_ownerProperty)
+                        .Subscribe(x => PublishNext(x));
+                }
+            }
+        }
     }
 }

+ 16 - 29
src/Avalonia.Controls/Grid.cs

@@ -637,7 +637,7 @@ namespace Avalonia.Controls
         /// </summary>
         internal bool MeasureOverrideInProgress
         {
-            get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); }
+            get { return CheckFlags(Flags.MeasureOverrideInProgress); }
             set { SetFlags(value, Flags.MeasureOverrideInProgress); }
         }
 
@@ -646,7 +646,7 @@ namespace Avalonia.Controls
         /// </summary>
         internal bool ArrangeOverrideInProgress
         {
-            get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); }
+            get { return CheckFlags(Flags.ArrangeOverrideInProgress); }
             set { SetFlags(value, Flags.ArrangeOverrideInProgress); }
         }
 
@@ -2350,25 +2350,12 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// CheckFlagsAnd returns <c>true</c> if all the flags in the
+        /// CheckFlags returns <c>true</c> if all the flags in the
         /// given bitmask are set on the object.
         /// </summary>
-        private bool CheckFlagsAnd(Flags flags)
+        private bool CheckFlags(Flags flags)
         {
-            return ((_flags & flags) == flags);
-        }
-
-        /// <summary>
-        /// CheckFlagsOr returns <c>true</c> if at least one flag in the
-        /// given bitmask is set.
-        /// </summary>
-        /// <remarks>
-        /// If no bits are set in the given bitmask, the method returns
-        /// <c>true</c>.
-        /// </remarks>
-        private bool CheckFlagsOr(Flags flags)
-        {
-            return (flags == 0 || (_flags & flags) != 0);
+            return _flags.HasFlagCustom(flags);
         }
 
         private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
@@ -2535,7 +2522,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool CellsStructureDirty
         {
-            get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); }
+            get { return !CheckFlags(Flags.ValidCellsStructure); }
             set { SetFlags(!value, Flags.ValidCellsStructure); }
         }
 
@@ -2544,7 +2531,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool ListenToNotifications
         {
-            get { return (CheckFlagsAnd(Flags.ListenToNotifications)); }
+            get { return CheckFlags(Flags.ListenToNotifications); }
             set { SetFlags(value, Flags.ListenToNotifications); }
         }
 
@@ -2553,7 +2540,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool SizeToContentU
         {
-            get { return (CheckFlagsAnd(Flags.SizeToContentU)); }
+            get { return CheckFlags(Flags.SizeToContentU); }
             set { SetFlags(value, Flags.SizeToContentU); }
         }
 
@@ -2562,7 +2549,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool SizeToContentV
         {
-            get { return (CheckFlagsAnd(Flags.SizeToContentV)); }
+            get { return CheckFlags(Flags.SizeToContentV); }
             set { SetFlags(value, Flags.SizeToContentV); }
         }
 
@@ -2571,7 +2558,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool HasStarCellsU
         {
-            get { return (CheckFlagsAnd(Flags.HasStarCellsU)); }
+            get { return CheckFlags(Flags.HasStarCellsU); }
             set { SetFlags(value, Flags.HasStarCellsU); }
         }
 
@@ -2580,7 +2567,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool HasStarCellsV
         {
-            get { return (CheckFlagsAnd(Flags.HasStarCellsV)); }
+            get { return CheckFlags(Flags.HasStarCellsV); }
             set { SetFlags(value, Flags.HasStarCellsV); }
         }
 
@@ -2589,7 +2576,7 @@ namespace Avalonia.Controls
         /// </summary>
         private bool HasGroup3CellsInAutoRows
         {
-            get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); }
+            get { return CheckFlags(Flags.HasGroup3CellsInAutoRows); }
             set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); }
         }
 
@@ -2803,10 +2790,10 @@ namespace Avalonia.Controls
             internal LayoutTimeSizeType SizeTypeU;
             internal LayoutTimeSizeType SizeTypeV;
             internal int Next;
-            internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } }
-            internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } }
-            internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } }
-            internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } }
+            internal bool IsStarU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Star);
+            internal bool IsAutoU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Auto);
+            internal bool IsStarV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Star);
+            internal bool IsAutoV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Auto);
         }
 
         /// <summary>

+ 4 - 4
src/Avalonia.Controls/ListBox.cs

@@ -135,8 +135,8 @@ namespace Avalonia.Controls
                 e.Handled = UpdateSelectionFromEventSource(
                     e.Source,
                     true,
-                    (e.KeyModifiers & KeyModifiers.Shift) != 0,
-                    (e.KeyModifiers & KeyModifiers.Control) != 0);
+                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
+                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Control));
             }
         }
 
@@ -154,8 +154,8 @@ namespace Avalonia.Controls
                     e.Handled = UpdateSelectionFromEventSource(
                         e.Source,
                         true,
-                        (e.KeyModifiers & KeyModifiers.Shift) != 0,
-                        (e.KeyModifiers & KeyModifiers.Control) != 0,
+                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
+                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
                         point.Properties.IsRightButtonPressed);
                 }
             }

+ 51 - 3
src/Avalonia.Controls/MenuItem.cs

@@ -102,6 +102,8 @@ namespace Avalonia.Controls
         private ICommand? _command;
         private bool _commandCanExecute = true;
         private Popup? _popup;
+        private KeyGesture _hotkey;
+        private bool _isEmbeddedInMenu;
 
         /// <summary>
         /// Initializes static members of the <see cref="MenuItem"/> class.
@@ -111,6 +113,7 @@ namespace Avalonia.Controls
             SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
             PressedMixin.Attach<MenuItem>();
             CommandProperty.Changed.Subscribe(CommandChanged);
+            CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
             FocusableProperty.OverrideDefaultValue<MenuItem>(true);
             HeaderProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.HeaderChanged(e));
             IconProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.IconChanged(e));
@@ -145,7 +148,7 @@ namespace Avalonia.Controls
                 {
                     var parent = x as Control;
                     return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
-                        Observable.Return<DefinitionBase.SharedSizeScope?>(null);
+                           Observable.Return<DefinitionBase.SharedSizeScope?>(null);
                 });
 
             this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
@@ -273,7 +276,7 @@ namespace Avalonia.Controls
         public bool IsTopLevel => Parent is Menu;
 
         /// <inheritdoc/>
-        bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; 
+        bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
 
         /// <inheritdoc/>
         IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
@@ -308,7 +311,7 @@ namespace Avalonia.Controls
                     .Select(x => x.ContainerControl)
                     .OfType<IMenuItem>();
             }
-        }            
+        }
 
         /// <summary>
         /// Opens the submenu.
@@ -335,18 +338,51 @@ namespace Avalonia.Controls
             return new MenuItemContainerGenerator(this);
         }
 
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+
+            if (!_isEmbeddedInMenu)
+            {
+                //Normally the Menu's IMenuInteractionHandler is sending the click events for us
+                //However when the item is not embedded into a menu we need to send them ourselves.
+                RaiseEvent(new RoutedEventArgs(ClickEvent));
+            }
+        }
+
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
+            if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
+            {
+                HotKey = _hotkey;
+            }
+            
             base.OnAttachedToLogicalTree(e);
 
             if (Command != null)
             {
                 Command.CanExecuteChanged += CanExecuteChanged;
             }
+
+            var parent = Parent;
+
+            while (parent is MenuItem)
+            {
+                parent = parent.Parent;
+            }
+
+            _isEmbeddedInMenu = parent is IMenu;
         }
 
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
+            // This will cause the hotkey manager to dispose the observer and the reference to this control
+            if (HotKey != null)
+            {
+                _hotkey = HotKey;
+                HotKey = null;
+            }
+
             base.OnDetachedFromLogicalTree(e);
 
             if (Command != null)
@@ -493,6 +529,18 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called when the <see cref="CommandParameter"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is MenuItem menuItem)
+            {
+                menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
         /// </summary>

+ 5 - 1
src/Avalonia.Controls/NativeControlHost.cs

@@ -157,10 +157,14 @@ namespace Avalonia.Controls
             var needsShow = IsEffectivelyVisible && bounds.HasValue;
 
             if (needsShow)
+            {
+                if (bounds.Value.IsEmpty)
+                    return false;
                 _attachment?.ShowInBounds(bounds.Value);
+            }
             else
                 _attachment?.HideWithSize(Bounds.Size);
-            return false;
+            return true;
         }
 
         private void CheckDestruction()

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

@@ -98,7 +98,7 @@ namespace Avalonia.Platform
         /// Sets the cursor associated with the toplevel.
         /// </summary>
         /// <param name="cursor">The cursor. Use null for default cursor</param>
-        void SetCursor(IPlatformHandle cursor);
+        void SetCursor(ICursorImpl cursor);
 
         /// <summary>
         /// Gets or sets a method called when the underlying implementation is destroyed.

+ 6 - 6
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -73,20 +73,20 @@ namespace Avalonia.Platform
         {
             if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None)
                 return effect; // No need to check for the modifiers.
-            if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(RawInputModifiers.Alt))
+            if (effect.HasFlagCustom(DragDropEffects.Link) && modifiers.HasFlagCustom(RawInputModifiers.Alt))
                 return DragDropEffects.Link;
-            if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(RawInputModifiers.Control))
+            if (effect.HasFlagCustom(DragDropEffects.Copy) && modifiers.HasFlagCustom(RawInputModifiers.Control))
                 return DragDropEffects.Copy;
             return DragDropEffects.Move;
         }
 
         private StandardCursorType GetCursorForDropEffect(DragDropEffects effects)
         {
-            if (effects.HasFlag(DragDropEffects.Copy))
+            if (effects.HasFlagCustom(DragDropEffects.Copy))
                 return StandardCursorType.DragCopy;
-            if (effects.HasFlag(DragDropEffects.Move))
+            if (effects.HasFlagCustom(DragDropEffects.Move))
                 return StandardCursorType.DragMove;
-            if (effects.HasFlag(DragDropEffects.Link))
+            if (effects.HasFlagCustom(DragDropEffects.Link))
                 return StandardCursorType.DragLink;
             return StandardCursorType.No;
         }
@@ -161,7 +161,7 @@ namespace Avalonia.Platform
             
             void CheckDraggingAccepted(RawInputModifiers changedMouseButton)
             {
-                if (_initialInputModifiers.Value.HasFlag(changedMouseButton))
+                if (_initialInputModifiers.Value.HasFlagCustom(changedMouseButton))
                 {
                     var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers);
                     UpdateCursor(null, DragDropEffects.None);

+ 3 - 1
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -80,7 +80,9 @@ namespace Avalonia.Controls.Presenters
         static TextPresenter()
         {
             AffectsRender<TextPresenter>(SelectionBrushProperty, TextBlock.ForegroundProperty, 
-                                         SelectionForegroundBrushProperty, CaretBrushProperty);
+                                         SelectionForegroundBrushProperty, CaretBrushProperty,
+                                         SelectionStartProperty, SelectionEndProperty);
+            
             AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty, 
                 TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
                 TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);

+ 10 - 11
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -253,9 +253,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
     {
         public static void ValidateEdge(this PopupAnchor edge)
         {
-            if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0)
-                ||
-                ((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0))
+            if (edge.HasFlagCustom(PopupAnchor.Left) && edge.HasFlagCustom(PopupAnchor.Right) ||
+                edge.HasFlagCustom(PopupAnchor.Top) && edge.HasFlagCustom(PopupAnchor.Bottom))
                 throw new ArgumentException("Opposite edges specified");
         }
 
@@ -266,25 +265,25 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
         public static PopupAnchor Flip(this PopupAnchor edge)
         {
-            var hmask = PopupAnchor.Left | PopupAnchor.Right;
-            var vmask = PopupAnchor.Top | PopupAnchor.Bottom;
-            if ((edge & hmask) != 0)
-                edge ^= hmask;
-            if ((edge & vmask) != 0)
-                edge ^= vmask;
+            if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
+                edge ^= PopupAnchor.HorizontalMask;
+
+            if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
+                edge ^= PopupAnchor.VerticalMask;
+
             return edge;
         }
 
         public static PopupAnchor FlipX(this PopupAnchor edge)
         {
-            if ((edge & PopupAnchor.HorizontalMask) != 0)
+            if (edge.HasFlagCustom(PopupAnchor.HorizontalMask))
                 edge ^= PopupAnchor.HorizontalMask;
             return edge;
         }
         
         public static PopupAnchor FlipY(this PopupAnchor edge)
         {
-            if ((edge & PopupAnchor.VerticalMask) != 0)
+            if (edge.HasFlagCustom(PopupAnchor.VerticalMask))
                 edge ^= PopupAnchor.VerticalMask;
             return edge;
         }

+ 20 - 28
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -42,16 +42,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
         private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
         {
             double x, y;
-            if ((edge & PopupAnchor.Left) != 0)
+            if (edge.HasFlagCustom(PopupAnchor.Left))
                 x = anchorRect.X;
-            else if ((edge & PopupAnchor.Right) != 0)
+            else if (edge.HasFlagCustom(PopupAnchor.Right))
                 x = anchorRect.Right;
             else
                 x = anchorRect.X + anchorRect.Width / 2;
             
-            if ((edge & PopupAnchor.Top) != 0)
+            if (edge.HasFlagCustom(PopupAnchor.Top))
                 y = anchorRect.Y;
-            else if ((edge & PopupAnchor.Bottom) != 0)
+            else if (edge.HasFlagCustom(PopupAnchor.Bottom))
                 y = anchorRect.Bottom;
             else
                 y = anchorRect.Y + anchorRect.Height / 2;
@@ -61,16 +61,16 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
         private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
         {
             double x, y;
-            if ((gravity & PopupGravity.Left) != 0)
+            if (gravity.HasFlagCustom(PopupGravity.Left))
                 x = -size.Width;
-            else if ((gravity & PopupGravity.Right) != 0)
+            else if (gravity.HasFlagCustom(PopupGravity.Right))
                 x = 0;
             else
                 x = -size.Width / 2;
             
-            if ((gravity & PopupGravity.Top) != 0)
+            if (gravity.HasFlagCustom(PopupGravity.Top))
                 y = -size.Height;
-            else if ((gravity & PopupGravity.Bottom) != 0)
+            else if (gravity.HasFlagCustom(PopupGravity.Bottom))
                 y = 0;
             else
                 y = -size.Height / 2;
@@ -125,21 +125,13 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
 
             bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
             {
-                if ((edge & PopupAnchor.Left) != 0
-                    && rc.X < bounds.X)
-                    return false;
-
-                if ((edge & PopupAnchor.Top) != 0
-                    && rc.Y < bounds.Y)
-                    return false;
-
-                if ((edge & PopupAnchor.Right) != 0
-                    && rc.Right > bounds.Right)
-                    return false;
-
-                if ((edge & PopupAnchor.Bottom) != 0
-                    && rc.Bottom > bounds.Bottom)
+                if (edge.HasFlagCustom(PopupAnchor.Left) && rc.X < bounds.X ||
+                    edge.HasFlagCustom(PopupAnchor.Top) && rc.Y < bounds.Y ||
+                    edge.HasFlagCustom(PopupAnchor.Right) && rc.Right > bounds.Right ||
+                    edge.HasFlagCustom(PopupAnchor.Bottom) && rc.Bottom > bounds.Bottom)
+                {
                     return false;
+                }
 
                 return true;
             }
@@ -155,7 +147,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             // If flipping geometry and anchor is allowed and helps, use the flipped one,
             // otherwise leave it as is
             if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
-                && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
+                && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipX))
             {
                 var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
                 if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
@@ -163,7 +155,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // If sliding is allowed, try moving the rect into the bounds
-            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0)
+            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideX))
             {
                 geo = geo.WithX(Math.Max(geo.X, bounds.X));
                 if (geo.Right > bounds.Right)
@@ -171,7 +163,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
             
             // Resize the rect horizontally if allowed.
-            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0)
+            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeX))
             {
                 var unconstrainedRect = geo;
 
@@ -194,7 +186,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             // If flipping geometry and anchor is allowed and helps, use the flipped one,
             // otherwise leave it as is
             if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
-                && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
+                && constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.FlipY))
             {
                 var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
                 if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
@@ -202,7 +194,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // If sliding is allowed, try moving the rect into the bounds
-            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0)
+            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.SlideY))
             {
                 geo = geo.WithY(Math.Max(geo.Y, bounds.Y));
                 if (geo.Bottom > bounds.Bottom)
@@ -210,7 +202,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             }
 
             // Resize the rect vertically if allowed.
-            if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0)
+            if (constraintAdjustment.HasFlagCustom(PopupPositionerConstraintAdjustment.ResizeY))
             {
                 var unconstrainedRect = geo;
 

+ 4 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -321,7 +321,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
         /// </summary>
-        protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
+        protected bool AlwaysSelected => SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected);
 
         /// <inheritdoc/>
         public override void BeginInit()
@@ -487,7 +487,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (ItemCount > 0 &&
                     Match(keymap.SelectAll) &&
-                    SelectionMode.HasFlag(SelectionMode.Multiple))
+                    SelectionMode.HasFlagCustom(SelectionMode.Multiple))
                 {
                     Selection.SelectAll();
                     e.Handled = true;
@@ -577,8 +577,8 @@ namespace Avalonia.Controls.Primitives
             }
 
             var mode = SelectionMode;
-            var multi = (mode & SelectionMode.Multiple) != 0;
-            var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
+            var multi = mode.HasFlagCustom(SelectionMode.Multiple);
+            var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
             var range = multi && rangeModifier;
 
             if (!select)

+ 2 - 2
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@@ -53,8 +53,8 @@ namespace Avalonia.Controls
         {
             return _owner.GetElementImpl(
                 index,
-                (options & ElementRealizationOptions.ForceCreate) != 0,
-                (options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
+                options.HasFlagCustom(ElementRealizationOptions.ForceCreate),
+                options.HasFlagCustom(ElementRealizationOptions.SuppressAutoRecycle));
         }
 
         protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);

+ 38 - 9
src/Avalonia.Controls/Slider.cs

@@ -49,6 +49,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Orientation> OrientationProperty =
             ScrollBar.OrientationProperty.AddOwner<Slider>();
 
+        /// <summary>
+        /// Defines the <see cref="IsDirectionReversed"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsDirectionReversedProperty =
+            Track.IsDirectionReversedProperty.AddOwner<Slider>();
+
         /// <summary>
         /// Defines the <see cref="IsSnapToTickEnabled"/> property.
         /// </summary>
@@ -83,7 +89,6 @@ namespace Avalonia.Controls
         private IDisposable _increaseButtonSubscription;
         private IDisposable _increaseButtonReleaseDispose;
         private IDisposable _pointerMovedDispose;
-        private IDisposable _trackOnKeyDownDispose;
 
         private const double Tolerance = 0.0001;
 
@@ -93,6 +98,7 @@ namespace Avalonia.Controls
         static Slider()
         {
             PressedMixin.Attach<Slider>();
+            FocusableProperty.OverrideDefaultValue<Slider>(true);
             OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
             Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
             Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
@@ -127,6 +133,19 @@ namespace Avalonia.Controls
             set { SetValue(OrientationProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the direction of increasing value.
+        /// </summary>
+        /// <value>
+        /// true if the direction of increasing value is to the left for a horizontal slider or
+        /// down for a vertical slider; otherwise, false. The default is false.
+        /// </value>
+        public bool IsDirectionReversed
+        {
+            get { return GetValue(IsDirectionReversedProperty); }
+            set { SetValue(IsDirectionReversedProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark.
         /// </summary>
@@ -165,7 +184,6 @@ namespace Avalonia.Controls
             _increaseButtonSubscription?.Dispose();
             _increaseButtonReleaseDispose?.Dispose();
             _pointerMovedDispose?.Dispose();
-            _trackOnKeyDownDispose?.Dispose();
             
             _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
             _track = e.NameScope.Find<Track>("PART_Track");
@@ -174,7 +192,6 @@ namespace Avalonia.Controls
             if (_track != null)
             {
                 _track.IsThumbDragHandled = true;
-                _trackOnKeyDownDispose = _track.AddDisposableHandler(KeyDownEvent, TrackOnKeyDown);
             }
 
             if (_decreaseButton != null)
@@ -192,26 +209,32 @@ namespace Avalonia.Controls
             _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
         }
 
-        private void TrackOnKeyDown(object sender, KeyEventArgs e)
+        protected override void OnKeyDown(KeyEventArgs e)
         {
-            if (e.KeyModifiers != KeyModifiers.None) return;
+            base.OnKeyDown(e);
+
+            if (e.Handled || e.KeyModifiers != KeyModifiers.None) return;
+
+            var handled = true;
 
             switch (e.Key)
             {
+                case Key.Down:
                 case Key.Left:
-                    MoveToNextTick(-SmallChange);
+                    MoveToNextTick(IsDirectionReversed ? SmallChange : -SmallChange);
                     break;
 
+                case Key.Up:
                 case Key.Right:
-                    MoveToNextTick(SmallChange);
+                    MoveToNextTick(IsDirectionReversed ? -SmallChange : SmallChange);
                     break;
 
                 case Key.PageUp:
-                    MoveToNextTick(-LargeChange);
+                    MoveToNextTick(IsDirectionReversed ? -LargeChange : LargeChange);
                     break;
 
                 case Key.PageDown:
-                    MoveToNextTick(LargeChange);
+                    MoveToNextTick(IsDirectionReversed ? LargeChange : -LargeChange);
                     break;
 
                 case Key.Home:
@@ -221,7 +244,13 @@ namespace Avalonia.Controls
                 case Key.End:
                     Value = Maximum;
                     break;
+
+                default:
+                    handled = false;
+                    break;
             }
+
+            e.Handled = handled;
         }
             
         private void MoveToNextTick(double direction)

+ 2 - 2
src/Avalonia.Controls/TextBox.cs

@@ -585,7 +585,7 @@ namespace Avalonia.Controls
             var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
 
             bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
-            bool DetectSelection() => e.KeyModifiers.HasFlag(keymap.SelectionModifiers);
+            bool DetectSelection() => e.KeyModifiers.HasFlagCustom(keymap.SelectionModifiers);
 
             if (Match(keymap.SelectAll))
             {
@@ -703,7 +703,7 @@ namespace Avalonia.Controls
             }
             else
             {
-                bool hasWholeWordModifiers = modifiers.HasFlag(keymap.WholeWordTextActionModifiers);
+                bool hasWholeWordModifiers = modifiers.HasFlagCustom(keymap.WholeWordTextActionModifiers);
                 switch (e.Key)
                 {
                     case Key.Left:

+ 1 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -165,7 +165,7 @@ namespace Avalonia.Controls
             this.GetObservable(PointerOverElementProperty)
                 .Select(
                     x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
-                .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor));
+                .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl));
 
             if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
             {

+ 6 - 6
src/Avalonia.Controls/TreeView.cs

@@ -412,7 +412,7 @@ namespace Avalonia.Controls
                 e.Handled = UpdateSelectionFromEventSource(
                     e.Source,
                     true,
-                    (e.KeyModifiers & KeyModifiers.Shift) != 0);
+                    e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift));
             }
         }
 
@@ -521,8 +521,8 @@ namespace Avalonia.Controls
                     e.Handled = UpdateSelectionFromEventSource(
                         e.Source,
                         true,
-                        (e.KeyModifiers & KeyModifiers.Shift) != 0,
-                        (e.KeyModifiers & KeyModifiers.Control) != 0,
+                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Shift),
+                        e.KeyModifiers.HasFlagCustom(KeyModifiers.Control),
                         point.Properties.IsRightButtonPressed);
                 }
             }
@@ -558,9 +558,9 @@ namespace Avalonia.Controls
             }
 
             var mode = SelectionMode;
-            var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
-            var multi = (mode & SelectionMode.Multiple) != 0;
-            var range = multi && selectedContainer != null && rangeModifier;
+            var toggle = toggleModifier || mode.HasFlagCustom(SelectionMode.Toggle);
+            var multi = mode.HasFlagCustom(SelectionMode.Multiple);
+            var range = multi && rangeModifier && selectedContainer != null;
 
             if (rightButton)
             {

+ 2 - 0
src/Avalonia.Controls/Utils/AncestorFinder.cs

@@ -47,6 +47,8 @@ namespace Avalonia.Controls.Utils
 
             public void Dispose()
             {
+                _child?.Dispose();
+                _subject.Dispose();
                 _disposable.Dispose();
             }
         }

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

@@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote
             var threading = new InternalPlatformThreadingInterface();
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardStub>()
-                .Bind<IStandardCursorFactory>().ToSingleton<CursorFactoryStub>()
+                .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(Keyboard)
                 .Bind<IPlatformSettings>().ToConstant(instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(threading)

+ 9 - 3
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -73,7 +73,7 @@ namespace Avalonia.DesignerSupport.Remote
 
         public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1);
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
         }
 
@@ -192,9 +192,15 @@ namespace Avalonia.DesignerSupport.Remote
         public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
     }
 
-    class CursorFactoryStub : IStandardCursorFactory
+    class CursorFactoryStub : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB");
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
+
+        private class CursorStub : ICursorImpl
+        {
+            public void Dispose() { }
+        }
     }
 
     class IconLoaderStub : IPlatformIconLoader

+ 1 - 1
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
                        {
                            Directory.GetFiles(x.VolumePath);
                        }
-                       catch (UnauthorizedAccessException _)
+                       catch (Exception _)
                        {
                            return null;
                        }

+ 4 - 4
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -223,13 +223,13 @@ namespace Avalonia.FreeDesktop
                             return null;
                         var lst = new List<string>();
                         var mod = item.Gesture;
-                        if ((mod.KeyModifiers & KeyModifiers.Control) != 0)
+                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
                             lst.Add("Control");
-                        if ((mod.KeyModifiers & KeyModifiers.Alt) != 0)
+                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
                             lst.Add("Alt");
-                        if ((mod.KeyModifiers & KeyModifiers.Shift) != 0)
+                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
                             lst.Add("Shift");
-                        if ((mod.KeyModifiers & KeyModifiers.Meta) != 0)
+                        if (mod.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
                             lst.Add("Super");
                         lst.Add(item.Gesture.Key.ToString());
                         return new[] { lst.ToArray() };

+ 2 - 2
src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs

@@ -33,11 +33,11 @@ namespace Avalonia.Headless.Vnc
                 {
                     Window?.MouseMove(pt);
                     foreach (var btn in CheckedButtons)
-                        if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn))
+                        if (_previousButtons.HasFlagCustom(btn) && !buttons.HasFlagCustom(btn))
                             Window?.MouseUp(pt, TranslateButton(btn), modifiers);
                     
                     foreach (var btn in CheckedButtons)
-                        if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn))
+                        if (!_previousButtons.HasFlagCustom(btn) && buttons.HasFlagCustom(btn))
                             Window?.MouseDown(pt, TranslateButton(btn), modifiers);
                     _previousButtons = buttons;
                 }, DispatcherPriority.Input);

+ 1 - 1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Headless
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
                 .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
-                .Bind<IStandardCursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
+                .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
                 .Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
                 .Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
                 .Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()

+ 5 - 3
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -52,12 +52,14 @@ namespace Avalonia.Headless
         }
     }
 
-    class HeadlessCursorFactoryStub : IStandardCursorFactory
+    class HeadlessCursorFactoryStub : ICursorFactory
     {
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
 
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        private class CursorStub : ICursorImpl
         {
-            return new PlatformHandle(new IntPtr((int)cursorType), "STUB");
+            public void Dispose() { }
         }
     }
 

+ 1 - 1
src/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Headless
 
         public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling);
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
 
         }

+ 1 - 1
src/Avalonia.Input/AccessKeyHandler.cs

@@ -177,7 +177,7 @@ namespace Avalonia.Input
         {
             bool menuIsOpen = MainMenu?.IsOpen == true;
 
-            if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen)
+            if (e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt) || menuIsOpen)
             {
                 // If any other key is pressed with the Alt key held down, or the main menu is open,
                 // find all controls who have registered that access key.

+ 4 - 0
src/Avalonia.Input/ApiCompatBaseline.txt

@@ -0,0 +1,4 @@
+Compat issues with assembly Avalonia.Input:
+MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract.
+Total Issues: 2

+ 18 - 21
src/Avalonia.Input/Cursors.cs → src/Avalonia.Input/Cursor.cs

@@ -1,15 +1,11 @@
 using System;
+using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 
+#nullable enable
+
 namespace Avalonia.Input
 {
-    /*
-    =========================================================================================
-        NOTE: Cursors are NOT disposable and are cached in platform implementation.
-        To support loading custom cursors some measures about that should be taken beforehand
-    =========================================================================================
-    */
-
     public enum StandardCursorType
     {
         Arrow,
@@ -46,21 +42,28 @@ namespace Avalonia.Input
         // SizeNorthEastSouthWest,
     }
 
-    public class Cursor
+    public class Cursor : IDisposable
     {
         public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow);
 
-        internal Cursor(IPlatformHandle platformCursor)
+        internal Cursor(ICursorImpl platformImpl)
         {
-            PlatformCursor = platformCursor;
+            PlatformImpl = platformImpl;
         }
 
         public Cursor(StandardCursorType cursorType)
-            : this(GetCursor(cursorType))
+            : this(GetCursorFactory().GetCursor(cursorType))
+        {
+        }
+
+        public Cursor(IBitmap cursor, PixelPoint hotSpot)
+            : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot))
         {
         }
 
-        public IPlatformHandle PlatformCursor { get; }
+        public ICursorImpl PlatformImpl { get; }
+
+        public void Dispose() => PlatformImpl.Dispose();
 
         public static Cursor Parse(string s)
         {
@@ -69,16 +72,10 @@ namespace Avalonia.Input
                 throw new ArgumentException($"Unrecognized cursor type '{s}'.");
         }
 
-        private static IPlatformHandle GetCursor(StandardCursorType type)
+        private static ICursorFactory GetCursorFactory()
         {
-            var platform = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
-
-            if (platform == null)
-            {
-                throw new Exception("Could not create Cursor: IStandardCursorFactory not registered.");
-            }
-
-            return platform.GetCursor(type);
+            return AvaloniaLocator.Current.GetService<ICursorFactory>() ??
+                throw new Exception("Could not create Cursor: ICursorFactory not registered.");
         }
     }
 }

+ 3 - 1
src/Avalonia.Input/FocusManager.cs

@@ -75,7 +75,9 @@ namespace Avalonia.Input
                 // If control is null, set focus to the topmost focus scope.
                 foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList())
                 {
-                    if (_focusScopes.TryGetValue(scope, out var element) && element != null)
+                    if (scope != Scope &&
+                        _focusScopes.TryGetValue(scope, out var element) &&
+                        element != null)
                     {
                         Focus(element, method);
                         return;

+ 12 - 0
src/Avalonia.Input/Platform/ICursorFactory.cs

@@ -0,0 +1,12 @@
+using Avalonia.Input;
+
+#nullable enable
+
+namespace Avalonia.Platform
+{
+    public interface ICursorFactory
+    {
+        ICursorImpl GetCursor(StandardCursorType cursorType);
+        ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot);
+    }
+}

+ 14 - 0
src/Avalonia.Input/Platform/ICursorImpl.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Input;
+
+#nullable enable
+
+namespace Avalonia.Platform
+{
+    /// <summary>
+    /// Represents a platform implementation of a <see cref="Cursor"/>.
+    /// </summary>
+    public interface ICursorImpl : IDisposable
+    {
+    }
+}

+ 0 - 9
src/Avalonia.Input/Platform/IStandardCursorFactory.cs

@@ -1,9 +0,0 @@
-using Avalonia.Input;
-
-namespace Avalonia.Platform
-{
-    public interface IStandardCursorFactory
-    {
-        IPlatformHandle GetCursor(StandardCursorType cursorType);
-    }
-}

+ 1 - 1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -97,7 +97,7 @@ namespace Avalonia.Native
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformThreadingInterface>()
                 .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
-                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
+                .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
                 .Bind<IPlatformSettings>().ToConstant(this)

+ 22 - 3
src/Avalonia.Native/Cursor.cs

@@ -1,11 +1,12 @@
 using System;
+using System.IO;
 using Avalonia.Input;
 using Avalonia.Platform;
 using Avalonia.Native.Interop;
 
 namespace Avalonia.Native
 {
-    class AvaloniaNativeCursor : IPlatformHandle, IDisposable
+    class AvaloniaNativeCursor : ICursorImpl, IDisposable
     {
         public IAvnCursor Cursor { get; private set; }
         public IntPtr Handle => IntPtr.Zero;
@@ -24,7 +25,7 @@ namespace Avalonia.Native
         }
     }
 
-    class CursorFactory : IStandardCursorFactory
+    class CursorFactory : ICursorFactory
     {
         IAvnCursorFactory _native;
 
@@ -33,10 +34,28 @@ namespace Avalonia.Native
             _native = native;
         }
 
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        public ICursorImpl GetCursor(StandardCursorType cursorType)
         {
             var cursor = _native.GetCursor((AvnStandardCursorType)cursorType);
             return new AvaloniaNativeCursor( cursor );
         }
+
+        public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
+        {
+            using(var ms = new MemoryStream())
+            {
+                cursor.Save(ms);
+
+                var imageData = ms.ToArray();
+
+                fixed(void* ptr = imageData)
+                {
+                    var avnCursor = _native.CreateCustomCursor(ptr, new IntPtr(imageData.Length),
+                        new AvnPixelSize { Width = hotSpot.X, Height = hotSpot.Y });
+
+                    return new AvaloniaNativeCursor(avnCursor);
+                }
+            }
+        }
     }
 }

+ 3 - 3
src/Avalonia.Native/WindowImplBase.cs

@@ -53,7 +53,7 @@ namespace Avalonia.Native
         private bool _gpu = false;
         private readonly MouseDevice _mouse;
         private readonly IKeyboardDevice _keyboard;
-        private readonly IStandardCursorFactory _cursorFactory;
+        private readonly ICursorFactory _cursorFactory;
         private Size _savedLogicalSize;
         private Size _lastRenderedLogicalSize;
         private double _savedScaling;
@@ -68,7 +68,7 @@ namespace Avalonia.Native
 
             _keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
             _mouse = new MouseDevice();
-            _cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
+            _cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>();
         }
 
         protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext)
@@ -398,7 +398,7 @@ namespace Avalonia.Native
         public Action Deactivated { get; set; }
         public Action Activated { get; set; }
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
             if (_native == null)
             {

+ 1 - 0
src/Avalonia.Native/avn.idl

@@ -619,6 +619,7 @@ interface IAvnCursor : IUnknown
 interface IAvnCursorFactory : IUnknown
 {
      HRESULT GetCursor(AvnStandardCursorType cursorType, IAvnCursor** retOut);
+     HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut);
 }
 
 [uuid(60452465-8616-40af-bc00-042e69828ce7)]

+ 1 - 1
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@@ -29,7 +29,7 @@ namespace Avalonia.ReactiveUI
 
         /// <inheritdoc/>
         public bool ExecuteHook(
-            object source, object target,
+            object? source, object target,
             Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
             Func<IObservedChange<object, object>[]> getCurrentViewProperties,
             BindingDirection direction)

+ 2 - 0
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@@ -3,6 +3,8 @@
     <TargetFramework>netstandard2.0</TargetFramework>
     <PackageId>Avalonia.ReactiveUI</PackageId>
     <SignAssembly>false</SignAssembly>
+    <Nullable>enable</Nullable>
+    <WarningsAsErrors>nullable</WarningsAsErrors>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />

+ 6 - 6
src/Avalonia.ReactiveUI/ReactiveUserControl.cs

@@ -17,8 +17,8 @@ namespace Avalonia.ReactiveUI
     /// <typeparam name="TViewModel">ViewModel type.</typeparam>
     public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class
     {
-        public static readonly StyledProperty<TViewModel> ViewModelProperty = AvaloniaProperty
-            .Register<ReactiveUserControl<TViewModel>, TViewModel>(nameof(ViewModel));
+        public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
+            .Register<ReactiveUserControl<TViewModel>, TViewModel?>(nameof(ViewModel));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ReactiveUserControl{TViewModel}"/> class.
@@ -34,16 +34,16 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// The ViewModel.
         /// </summary>
-        public TViewModel ViewModel
+        public TViewModel? ViewModel
         {
             get => GetValue(ViewModelProperty);
             set => SetValue(ViewModelProperty, value);
         }
 
-        object IViewFor.ViewModel
+        object? IViewFor.ViewModel
         {
             get => ViewModel;
-            set => ViewModel = (TViewModel)value;
+            set => ViewModel = (TViewModel?)value;
         }
 
         protected override void OnDataContextChanged(EventArgs e)
@@ -51,7 +51,7 @@ namespace Avalonia.ReactiveUI
             ViewModel = DataContext as TViewModel;
         }
 
-        private void OnViewModelChanged(object value)
+        private void OnViewModelChanged(object? value)
         {
             if (value == null)
             {

+ 7 - 7
src/Avalonia.ReactiveUI/ReactiveWindow.cs

@@ -17,8 +17,8 @@ namespace Avalonia.ReactiveUI
     /// <typeparam name="TViewModel">ViewModel type.</typeparam>
     public class ReactiveWindow<TViewModel> : Window, IViewFor<TViewModel> where TViewModel : class
     {
-        public static readonly StyledProperty<TViewModel> ViewModelProperty = AvaloniaProperty
-            .Register<ReactiveWindow<TViewModel>, TViewModel>(nameof(ViewModel));
+        public static readonly StyledProperty<TViewModel?> ViewModelProperty = AvaloniaProperty
+            .Register<ReactiveWindow<TViewModel>, TViewModel?>(nameof(ViewModel));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ReactiveWindow{TViewModel}"/> class.
@@ -35,19 +35,19 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// The ViewModel.
         /// </summary>
-        public TViewModel ViewModel
+        public TViewModel? ViewModel
         {
             get => GetValue(ViewModelProperty);
             set => SetValue(ViewModelProperty, value);
         }
 
-        object IViewFor.ViewModel
+        object? IViewFor.ViewModel
         {
             get => ViewModel;
-            set => ViewModel = (TViewModel)value;
+            set => ViewModel = (TViewModel?)value;
         }
 
-        private void OnDataContextChanged(object value)
+        private void OnDataContextChanged(object? value)
         {
             if (value is TViewModel viewModel)
             {
@@ -59,7 +59,7 @@ namespace Avalonia.ReactiveUI
             }
         }
 
-        private void OnViewModelChanged(object value)
+        private void OnViewModelChanged(object? value)
         {
             if (value == null)
             {

+ 8 - 8
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@@ -55,8 +55,8 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
         /// </summary>
-        public static readonly StyledProperty<RoutingState> RouterProperty =
-            AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
+        public static readonly StyledProperty<RoutingState?> RouterProperty =
+            AvaloniaProperty.Register<RoutedViewHost, RoutingState?>(nameof(Router));
     
         /// <summary>
         /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@@ -67,12 +67,12 @@ namespace Avalonia.ReactiveUI
             {
                 var routerRemoved = this
                     .WhenAnyValue(x => x.Router)
-                    .Where(router => router == null)
-                    .Cast<object>();
+                    .Where(router => router == null)!
+                    .Cast<object?>();
 
                 this.WhenAnyValue(x => x.Router)
                     .Where(router => router != null)
-                    .SelectMany(router => router.CurrentViewModel)
+                    .SelectMany(router => router!.CurrentViewModel)
                     .Merge(routerRemoved)
                     .Subscribe(NavigateToViewModel)
                     .DisposeWith(disposables);
@@ -82,7 +82,7 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the <see cref="RoutingState"/> of the view model stack.
         /// </summary>
-        public RoutingState Router
+        public RoutingState? Router
         {
             get => GetValue(RouterProperty);
             set => SetValue(RouterProperty, value);
@@ -91,13 +91,13 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the ReactiveUI view locator used by this router.
         /// </summary>
-        public IViewLocator ViewLocator { get; set; }
+        public IViewLocator? ViewLocator { get; set; }
     
         /// <summary>
         /// Invoked when ReactiveUI router navigates to a view model.
         /// </summary>
         /// <param name="viewModel">ViewModel to which the user navigates.</param>
-        private void NavigateToViewModel(object viewModel)
+        private void NavigateToViewModel(object? viewModel)
         {
             if (Router == null)
             {

+ 8 - 8
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@@ -13,20 +13,20 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
         /// </summary>
-        public static readonly StyledProperty<IPageTransition> PageTransitionProperty =
-            AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition),
+        public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
+            AvaloniaProperty.Register<TransitioningContentControl, IPageTransition?>(nameof(PageTransition),
                 new CrossFade(TimeSpan.FromSeconds(0.5)));
 
         /// <summary>
         /// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
         /// </summary>
-        public static readonly StyledProperty<object> DefaultContentProperty =
-            AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent));
+        public static readonly StyledProperty<object?> DefaultContentProperty =
+            AvaloniaProperty.Register<TransitioningContentControl, object?>(nameof(DefaultContent));
         
         /// <summary>
         /// Gets or sets the animation played when content appears and disappears.
         /// </summary>
-        public IPageTransition PageTransition
+        public IPageTransition? PageTransition
         {
             get => GetValue(PageTransitionProperty);
             set => SetValue(PageTransitionProperty, value);
@@ -35,7 +35,7 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the content displayed whenever there is no page currently routed.
         /// </summary>
-        public object DefaultContent
+        public object? DefaultContent
         {
             get => GetValue(DefaultContentProperty);
             set => SetValue(DefaultContentProperty, value);
@@ -44,7 +44,7 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the content with animation.
         /// </summary>
-        public new object Content
+        public new object? Content
         {
             get => base.Content;
             set => UpdateContentWithTransition(value);
@@ -60,7 +60,7 @@ namespace Avalonia.ReactiveUI
         /// Updates the content with transitions.
         /// </summary>
         /// <param name="content">New content to set.</param>
-        private async void UpdateContentWithTransition(object content)
+        private async void UpdateContentWithTransition(object? content)
         {
             if (PageTransition != null)
                 await PageTransition.Start(this, null, true);

+ 6 - 6
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@@ -15,8 +15,8 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
         /// </summary>
-        public static readonly AvaloniaProperty<object> ViewModelProperty =
-            AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel));
+        public static readonly AvaloniaProperty<object?> ViewModelProperty =
+            AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
@@ -34,7 +34,7 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the ViewModel to display.
         /// </summary>
-        public object ViewModel
+        public object? ViewModel
         {
             get => GetValue(ViewModelProperty);
             set => SetValue(ViewModelProperty, value);
@@ -43,13 +43,13 @@ namespace Avalonia.ReactiveUI
         /// <summary>
         /// Gets or sets the view locator.
         /// </summary>
-        public IViewLocator ViewLocator { get; set; }
+        public IViewLocator? ViewLocator { get; set; }
 
         /// <summary>
         /// Invoked when ReactiveUI router navigates to a view model.
         /// </summary>
         /// <param name="viewModel">ViewModel to which the user navigates.</param>
-        private void NavigateToViewModel(object viewModel)
+        private void NavigateToViewModel(object? viewModel)
         {
             if (viewModel == null)
             {
@@ -74,4 +74,4 @@ namespace Avalonia.ReactiveUI
             Content = viewInstance;
         }
     }
-}
+}

+ 4 - 1
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -1,8 +1,11 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Design.PreviewWith>
     <Border Padding="20">
-      <StackPanel>
+      <StackPanel Spacing="10">
         <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
+        <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
+        <ProgressBar VerticalAlignment="Center" Value="50" />
+        <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
         <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
       </StackPanel>
     </Border>

+ 3 - 2
src/Avalonia.Themes.Default/Slider.xaml

@@ -11,7 +11,7 @@
             <RowDefinition Height="Auto"/>
           </Grid.RowDefinitions>
           <Border Name="TrackBackground" Grid.Row="1" Height="4" Margin="6,0" VerticalAlignment="Center"/>
-          <Track Name="PART_Track" Grid.Row="1" Orientation="Horizontal">
+          <Track Name="PART_Track" Grid.Row="1" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Horizontal">
             <Track.DecreaseButton>
                <RepeatButton Name="PART_DecreaseButton"
                              Classes="repeattrack" />
@@ -46,7 +46,7 @@
             <ColumnDefinition Width="Auto"/>
           </Grid.ColumnDefinitions>
           <Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/>
-          <Track Name="PART_Track" Grid.Column="1" Orientation="Vertical">
+          <Track Name="PART_Track" Grid.Column="1" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Vertical">
             <Track.DecreaseButton>
                <RepeatButton Name="PART_DecreaseButton"
                              Classes="repeattrack" />
@@ -80,6 +80,7 @@
   </Style>
   <Style Selector="Slider /template/ RepeatButton.repeattrack">
     <Setter Property="Background" Value="Transparent"/>
+    <Setter Property="Focusable" Value="False"/>
     <Setter Property="Foreground" Value="{DynamicResource ThemeBorderLowBrush}"/>
     <Setter Property="Template">
         <ControlTemplate>

+ 4 - 2
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@@ -1,8 +1,11 @@
 <Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Design.PreviewWith>
     <Border Padding="20">
-      <StackPanel>
+      <StackPanel Spacing="10">
         <ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
+        <ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
+        <ProgressBar VerticalAlignment="Center" Value="50" />
+        <ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />
         <ProgressBar HorizontalAlignment="Left" IsIndeterminate="True" Orientation="Vertical" />
       </StackPanel>
     </Border>
@@ -12,7 +15,6 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
     <Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" />
     <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightTransparentBrush}" />
-    <Setter Property="Maximum" Value="100" />
     <Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" />
     <Setter Property="VerticalAlignment" Value="Center" />
     <Setter Property="Template">

+ 6 - 6
src/Avalonia.Themes.Fluent/Controls/Slider.xaml

@@ -62,9 +62,9 @@
                   <TickBar Name="TopTickBar" Placement="Top" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Bottom" Margin="0,0,0,4" Grid.ColumnSpan="3" />
                   <!-- <TickBar Name="HorizontalInlineTickBar" Placement="Top" Fill="{DynamicResource SliderInlineTickBarFill}" Height="{DynamicResource SliderTrackThemeHeight}" Grid.Row="1" Grid.ColumnSpan="3" /> -->
                   <TickBar Name="BottomTickBar" Placement="Bottom" Height="{DynamicResource SliderOutsideTickBarThemeHeight}" VerticalAlignment="Top" Margin="0,4,0,0" Grid.Row="2" Grid.ColumnSpan="3" />
-                  <Track Name="PART_Track" Grid.Row="1" Grid.ColumnSpan="3" Orientation="Horizontal">
+                  <Track Name="PART_Track" Grid.Row="1" Grid.ColumnSpan="3" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Horizontal">
                     <Track.DecreaseButton>
-                      <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+                      <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" Focusable="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                         <RepeatButton.Template>
                           <ControlTemplate>
                             <Grid>
@@ -76,7 +76,7 @@
                       </RepeatButton>
                     </Track.DecreaseButton>
                     <Track.IncreaseButton>
-                      <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
+                      <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" Focusable="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                         <RepeatButton.Template>
                           <ControlTemplate>
                             <Grid>
@@ -124,9 +124,9 @@
                   <TickBar Name="LeftTickBar" Placement="Left" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Right" Margin="0,0,4,0" Grid.RowSpan="3" />
                   <!-- <TickBar Name="VerticalInlineTickBar" Placement="Inline" Fill="{DynamicResource SliderInlineTickBarFill}" Width="{DynamicResource SliderTrackThemeHeight}" Grid.Column="1" Grid.RowSpan="3" /> -->
                   <TickBar Name="RightTickBar" Placement="Right" Width="{DynamicResource SliderOutsideTickBarThemeHeight}" HorizontalAlignment="Left" Margin="4,0,0,0" Grid.Column="2" Grid.RowSpan="3" />
-                  <Track Name="PART_Track" Grid.Column="1" Grid.ColumnSpan="1" Grid.RowSpan="3" Orientation="Vertical">
+                  <Track Name="PART_Track" Grid.Column="1" Grid.ColumnSpan="1" Grid.RowSpan="3" IsDirectionReversed="{TemplateBinding IsDirectionReversed}" Orientation="Vertical">
                     <Track.DecreaseButton>
-                      <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
+                      <RepeatButton Name="PART_DecreaseButton" Background="{TemplateBinding Foreground}" Focusable="False" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
                         <RepeatButton.Template>
                           <ControlTemplate>
                             <Grid>
@@ -138,7 +138,7 @@
                       </RepeatButton>
                     </Track.DecreaseButton>
                     <Track.IncreaseButton>
-                      <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
+                      <RepeatButton Name="PART_IncreaseButton" Background="{TemplateBinding Background}" Focusable="False" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
                         <RepeatButton.Template>
                           <ControlTemplate>
                             <Grid>

+ 2 - 2
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -36,7 +36,7 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        /// 
+        /// Initializes a new instance of the <see cref="FormattedText"/> class.
         /// </summary>
         /// <param name="text"></param>
         /// <param name="typeface"></param>
@@ -45,7 +45,7 @@ namespace Avalonia.Media
         /// <param name="textWrapping"></param>
         /// <param name="constraint"></param>
         public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
-            TextWrapping textWrapping, Size constraint)
+            TextWrapping textWrapping, Size constraint) : this()
         {
             _text = text;
 

+ 18 - 0
src/Avalonia.Visuals/Media/PathGeometryCollections.cs

@@ -1,9 +1,27 @@
 using Avalonia.Collections;
+using Avalonia.Visuals.Platform;
 
 namespace Avalonia.Media
 {
     public sealed class PathFigures : AvaloniaList<PathFigure>
     {
+        /// <summary>
+        /// Parses the specified path data to a <see cref="PathFigures"/>.
+        /// </summary>
+        /// <param name="pathData">The s.</param>
+        /// <returns></returns>
+        public static PathFigures Parse(string pathData)
+        {
+            var pathGeometry = new PathGeometry();
+            
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse(pathData);
+            }
+
+            return pathGeometry.Figures;
+        }
     }
 
     public sealed class PathSegments : AvaloniaList<PathSegment>

+ 0 - 56
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BreakPairTable.cs

@@ -1,56 +0,0 @@
-namespace Avalonia.Media.TextFormatting.Unicode
-{
-    internal static class BreakPairTable
-    {
-        private static readonly byte[][] s_breakPairTable = 
-            {
-             new byte[] {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,4,4,4,4,4,4,4,4,4,4,4},
-             new byte[] {0,4,4,1,1,4,4,4,4,1,1,0,0,0,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,4,4,4,4,1,1,1,1,1,0,4,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {4,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,0,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,1,0,1,1,0,0,4,2,4,1,1,1,1,1,0,1,1,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,0,0,1,1,1,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,0,1,4,4,4,0,0,1,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,0,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,4,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,1,1,1,1,1,1,4,2,4,1,1,1,1,1,1,1,1,1,1},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,1,1,1,1,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,1,1,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,1,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,0,0,0,0,0,0,1,1,0,0,4,2,4,0,0,0,0,0,1,0,0,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,1,1,0},
-             new byte[] {0,4,4,1,1,1,4,4,4,0,1,0,0,0,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {1,4,4,1,1,1,4,4,4,1,1,1,1,1,0,1,1,1,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-             new byte[] {0,4,4,1,1,0,4,4,4,0,0,0,0,0,0,0,0,0,0,0,4,2,4,0,0,0,0,0,0,0,0,1,0},
-        };
-
-        public static PairBreakType Map(LineBreakClass first, LineBreakClass second)
-        {
-            return (PairBreakType)s_breakPairTable[(int)first][(int)second];
-        }
-    }
-
-    internal enum PairBreakType : byte
-    {
-        DI = 0, // Direct break opportunity
-        IN = 1, // Indirect break opportunity
-        CI = 2, // Indirect break opportunity for combining marks
-        CP = 3, // Prohibited break for combining marks
-        PR = 4 // Prohibited break
-    }
-}

+ 15 - 12
src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs

@@ -9,37 +9,40 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// </summary>
         public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD');
 
-        private readonly int _value;
-
         public Codepoint(int value)
         {
-            _value = value;
+            Value = value;
         }
 
+        /// <summary>
+        /// Get the codepoint's value.
+        /// </summary>
+        public int Value { get; }
+
         /// <summary>
         /// Gets the <see cref="Unicode.GeneralCategory"/>.
         /// </summary>
-        public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(_value);
+        public GeneralCategory GeneralCategory => UnicodeData.GetGeneralCategory(Value);
 
         /// <summary>
         /// Gets the <see cref="Unicode.Script"/>.
         /// </summary>
-        public Script Script => UnicodeData.GetScript(_value);
+        public Script Script => UnicodeData.GetScript(Value);
 
         /// <summary>
         /// Gets the <see cref="Unicode.BiDiClass"/>.
         /// </summary>
-        public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(_value);
+        public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(Value);
 
         /// <summary>
         /// Gets the <see cref="Unicode.LineBreakClass"/>.
         /// </summary>
-        public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(_value);
+        public LineBreakClass LineBreakClass => UnicodeData.GetLineBreakClass(Value);
 
         /// <summary>
         /// Gets the <see cref="GraphemeBreakClass"/>.
         /// </summary>
-        public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(_value);
+        public GraphemeBreakClass GraphemeBreakClass => UnicodeData.GetGraphemeClusterBreak(Value);
 
         /// <summary>
         /// Determines whether this <see cref="Codepoint"/> is a break char.
@@ -51,7 +54,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         {
             get
             {
-                switch (_value)
+                switch (Value)
                 {
                     case '\u000A':
                     case '\u000B':
@@ -93,12 +96,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
         public static implicit operator int(Codepoint codepoint)
         {
-            return codepoint._value;
+            return codepoint.Value;
         }
 
         public static implicit operator uint(Codepoint codepoint)
         {
-            return (uint)codepoint._value;
+            return (uint)codepoint.Value;
         }
 
         /// <summary>
@@ -112,7 +115,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         {
             count = 1;
 
-            if (index > text.Length)
+            if (index >= text.Length)
             {
                 return ReplacementCodepoint;
             }

+ 394 - 146
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@@ -1,160 +1,460 @@
-// RichTextKit
-// Copyright © 2019 Topten Software. All Rights Reserved.
-// 
-// Licensed under the Apache License, Version 2.0 (the "License"); you may 
-// not use this product except in compliance with the License. You may obtain 
-// a copy of the License at
-// 
-// http://www.apache.org/licenses/LICENSE-2.0
-// 
-// Unless required by applicable law or agreed to in writing, software 
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
-// License for the specific language governing permissions and limitations 
-// under the License.
-//
-// Ported from: https://github.com/foliojs/linebreak
-// Copied from: https://github.com/toptensoftware/RichTextKit
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
 
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     /// <summary>
-    /// Implementation of the Unicode Line Break Algorithm
+    /// Implementation of the Unicode Line Break Algorithm. UAX:14
+    /// <see href="https://www.unicode.org/reports/tr14/tr14-37.html"/>
     /// </summary>
     public ref struct LineBreakEnumerator
     {
-        // State
         private readonly ReadOnlySlice<char> _text;
-        private int _pos;
-        private int _lastPos;
-        private LineBreakClass? _curClass;
-        private LineBreakClass? _nextClass;
+        private int _position;
+        private int _lastPosition;
+        private LineBreakClass _currentClass;
+        private LineBreakClass _nextClass;
+        private bool _first;
+        private int _alphaNumericCount;
+        private bool _lb8a;
+        private bool _lb21a;
+        private bool _lb22ex;
+        private bool _lb24ex;
+        private bool _lb25ex;
+        private bool _lb30;
+        private int _lb30a;
+        private bool _lb31;
 
         public LineBreakEnumerator(ReadOnlySlice<char> text)
+            : this()
         {
             _text = text;
-            _pos = 0;
-            _lastPos = 0;
-            _curClass = null;
-            _nextClass = null;
-            Current = default;
+            _position = 0;
+            _currentClass = LineBreakClass.Unknown;
+            _nextClass = LineBreakClass.Unknown;
+            _first = true;
+            _lb8a = false;
+            _lb21a = false;
+            _lb22ex = false;
+            _lb24ex = false;
+            _lb25ex = false;
+            _alphaNumericCount = 0;
+            _lb31 = false;
+            _lb30 = false;
+            _lb30a = 0;
         }
-
+        
         public LineBreak Current { get; private set; }
-
+        
         public bool MoveNext()
         {
-            // get the first char if we're at the beginning of the string
-            if (!_curClass.HasValue)
+            // Get the first char if we're at the beginning of the string.
+            if (_first)
             {
-                _curClass = PeekCharClass() == LineBreakClass.Space ? LineBreakClass.WordJoiner : MapFirst(ReadCharClass());
+                var firstClass = NextCharClass();
+                _first = false;
+                _currentClass = MapFirst(firstClass);
+                _nextClass = firstClass;
+                _lb8a = firstClass == LineBreakClass.ZWJ;
+                _lb30a = 0;
             }
 
-            while (_pos < _text.Length)
+            while (_position < _text.Length)
             {
-                _lastPos = _pos;
+                _lastPosition = _position;
                 var lastClass = _nextClass;
-                _nextClass = ReadCharClass();
+                _nextClass = NextCharClass();
 
-                // explicit newline
-                if (_curClass.HasValue && (_curClass == LineBreakClass.MandatoryBreak || _curClass == LineBreakClass.CarriageReturn && _nextClass != LineBreakClass.LineFeed))
+                // Explicit newline
+                switch (_currentClass)
                 {
-                    _curClass = MapFirst(MapClass(_nextClass.Value));
-                    Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true);
+                    case LineBreakClass.MandatoryBreak:
+                    case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed:
+                    {
+                        _currentClass = MapFirst(_nextClass);
+                        Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
+                        return true;
+                    }
+                }
+
+                var shouldBreak = GetSimpleBreak() ?? (bool?)GetPairTableBreak(lastClass);
+
+                // Rule LB8a
+                _lb8a = _nextClass == LineBreakClass.ZWJ;
+
+                if (shouldBreak.Value)
+                {
+                    Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
                     return true;
                 }
+            }
 
-                // handle classes not handled by the pair table
-                LineBreakClass? cur = null;
-                switch (_nextClass.Value)
+            if (_position >= _text.Length)
+            {
+                if (_lastPosition < _text.Length)
                 {
-                    case LineBreakClass.Space:
-                        cur = _curClass;
-                        break;
+                    _lastPosition = _text.Length;
+
+                    var required = false;
+
+                    switch (_currentClass)
+                    {
+                        case LineBreakClass.MandatoryBreak:
+                        case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed:
+                            required = true;
+                            break;
+                    }
+
+                    Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
+                    return true;
+                }
+            }
+
+            Current = default;
+            
+            return false;
+        }
+
+        private static LineBreakClass MapClass(Codepoint cp)
+        {
+            if (cp.Value == 327685)
+            {
+                return LineBreakClass.Alphabetic;
+            }
+            
+            // LB 1
+            // ==========================================
+            // Resolved Original    General_Category
+            // ==========================================
+            // AL       AI, SG, XX  Any
+            // CM       SA          Only Mn or Mc
+            // AL       SA          Any except Mn and Mc
+            // NS       CJ          Any
+            switch (cp.LineBreakClass)
+            {
+                case LineBreakClass.Ambiguous:
+                case LineBreakClass.Surrogate:
+                case LineBreakClass.Unknown:
+                    return LineBreakClass.Alphabetic;
 
+                case LineBreakClass.ComplexContext:
+                    return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark
+                        ? LineBreakClass.CombiningMark
+                        : LineBreakClass.Alphabetic;
+
+                case LineBreakClass.ConditionalJapaneseStarter:
+                    return LineBreakClass.Nonstarter;
+
+                default:
+                    return cp.LineBreakClass;
+            }
+        }
+
+        private static LineBreakClass MapFirst(LineBreakClass c)
+        {
+            switch (c)
+            {
+                case LineBreakClass.LineFeed:
+                case LineBreakClass.NextLine:
+                    return LineBreakClass.MandatoryBreak;
+
+                case LineBreakClass.Space:
+                    return LineBreakClass.WordJoiner;
+
+                default:
+                    return c;
+            }
+        }
+
+        private static bool IsAlphaNumeric(LineBreakClass cls)
+            => cls == LineBreakClass.Alphabetic
+            || cls == LineBreakClass.HebrewLetter
+            || cls == LineBreakClass.Numeric;
+
+        private LineBreakClass PeekNextCharClass()
+        {
+            var cp = Codepoint.ReadAt(_text, _position, out _);
+            
+            return MapClass(cp);
+        }
+
+        // Get the next character class
+        private LineBreakClass NextCharClass()
+        {
+            var cp = Codepoint.ReadAt(_text, _position, out var count);
+            var cls = MapClass(cp);
+            _position += count;
+
+            // Keep track of alphanumeric + any combining marks.
+            // This is used for LB22 and LB30.
+            if (IsAlphaNumeric(_currentClass) || _alphaNumericCount > 0 && cls == LineBreakClass.CombiningMark)
+            {
+                _alphaNumericCount++;
+            }
+
+            // Track combining mark exceptions. LB22
+            if (cls == LineBreakClass.CombiningMark)
+            {
+                switch (_currentClass)
+                {
                     case LineBreakClass.MandatoryBreak:
+                    case LineBreakClass.ContingentBreak:
+                    case LineBreakClass.Exclamation:
                     case LineBreakClass.LineFeed:
                     case LineBreakClass.NextLine:
-                        cur = LineBreakClass.MandatoryBreak;
-                        break;
-
+                    case LineBreakClass.Space:
+                    case LineBreakClass.ZWSpace:
                     case LineBreakClass.CarriageReturn:
-                        cur = LineBreakClass.CarriageReturn;
+                        _lb22ex = true;
                         break;
+                }
+            }
 
+            // Track combining mark exceptions. LB31
+            if (_first && cls == LineBreakClass.CombiningMark)
+            {
+                _lb31 = true;
+            }
+
+            if (cls == LineBreakClass.CombiningMark)
+            {
+                switch (_currentClass)
+                {
+                    case LineBreakClass.MandatoryBreak:
                     case LineBreakClass.ContingentBreak:
-                        cur = LineBreakClass.BreakAfter;
+                    case LineBreakClass.Exclamation:
+                    case LineBreakClass.LineFeed:
+                    case LineBreakClass.NextLine:
+                    case LineBreakClass.Space:
+                    case LineBreakClass.ZWSpace:
+                    case LineBreakClass.CarriageReturn:
+                    case LineBreakClass.ZWJ:
+                        _lb31 = true;
                         break;
                 }
+            }
+
+            if (_first
+                && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
+            {
+                _lb31 = true;
+            }
+
+            if (_currentClass == LineBreakClass.Alphabetic && 
+                (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
+            {
+                _lb31 = true;
+            }
+
+            // Reset LB31 if next is U+0028 (Left Opening Parenthesis)
+            if (_lb31
+                && _currentClass != LineBreakClass.PostfixNumeric
+                && _currentClass != LineBreakClass.PrefixNumeric
+                && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028)
+            {
+                _lb31 = false;
+            }
+
+            // Rule LB24
+            if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis))
+            {
+                _lb24ex = true;
+            }
 
-                if (cur != null)
+            // Rule LB25
+            if (_first
+                && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols))
+            {
+                _lb25ex = true;
+            }
+
+            if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic)
+            {
+                var next = PeekNextCharClass();
+                if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols)
                 {
-                    _curClass = cur;
+                    _lb25ex = true;
+                }
+            }
+
+            // AlphaNumeric + and combining marks can break for OP except.
+            // - U+0028 (Left Opening Parenthesis)
+            // - U+005B (Opening Square Bracket)
+            // - U+007B (Left Curly Bracket)
+            // See custom colums|rules in the text pair table.
+            // https://www.unicode.org/Public/13.0.0/ucd/auxiliary/LineBreakTest.html
+            _lb30 = _alphaNumericCount > 0
+                && cls == LineBreakClass.OpenPunctuation
+                && cp.Value != 0x0028
+                && cp.Value != 0x005B
+                && cp.Value != 0x007B;
+
+            return cls;
+        }
+
+        private bool? GetSimpleBreak()
+        {
+            // handle classes not handled by the pair table
+            switch (_nextClass)
+            {
+                case LineBreakClass.Space:
+                    return false;
 
-                    if (_nextClass.Value == LineBreakClass.MandatoryBreak)
+                case LineBreakClass.MandatoryBreak:
+                case LineBreakClass.LineFeed:
+                case LineBreakClass.NextLine:
+                    _currentClass = LineBreakClass.MandatoryBreak;
+                    return false;
+
+                case LineBreakClass.CarriageReturn:
+                    _currentClass = LineBreakClass.CarriageReturn;
+                    return false;
+            }
+
+            return null;
+        }
+
+        private bool GetPairTableBreak(LineBreakClass lastClass)
+        {
+            // If not handled already, use the pair table
+            bool shouldBreak = false;
+            switch (LineBreakPairTable.Table[(int)_currentClass][(int)_nextClass])
+            {
+                case LineBreakPairTable.DIBRK: // Direct break
+                    shouldBreak = true;
+                    break;
+
+                // TODO: Rewrite this so that it defaults to true and rules are set as exceptions.
+                case LineBreakPairTable.INBRK: // Possible indirect break
+
+                    // LB31
+                    if (_lb31 && _nextClass == LineBreakClass.OpenPunctuation)
                     {
-                        _lastPos = _pos;
-                        Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos, true);
-                        return true;
+                        shouldBreak = true;
+                        _lb31 = false;
+                        break;
                     }
 
-                    continue;
-                }
-
-                // if not handled already, use the pair table
-                var shouldBreak = false;
-                switch (BreakPairTable.Map(_curClass.Value,_nextClass.Value))
-                {
-                    case PairBreakType.DI: // Direct break
+                    // LB30
+                    if (_lb30)
+                    {
                         shouldBreak = true;
+                        _lb30 = false;
+                        _alphaNumericCount = 0;
                         break;
+                    }
 
-                    case PairBreakType.IN: // possible indirect break
-                        shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space;
+                    // LB25
+                    if (_lb25ex && (_nextClass == LineBreakClass.PrefixNumeric || _nextClass == LineBreakClass.Numeric))
+                    {
+                        shouldBreak = true;
+                        _lb25ex = false;
                         break;
+                    }
 
-                    case PairBreakType.CI:
-                        shouldBreak = lastClass.HasValue && lastClass.Value == LineBreakClass.Space;
-                        if (!shouldBreak)
-                        {
-                            continue;
-                        }
+                    // LB24
+                    if (_lb24ex && (_nextClass == LineBreakClass.PostfixNumeric || _nextClass == LineBreakClass.PrefixNumeric))
+                    {
+                        shouldBreak = true;
+                        _lb24ex = false;
                         break;
+                    }
+
+                    // LB18
+                    shouldBreak = lastClass == LineBreakClass.Space;
+                    break;
+
+                case LineBreakPairTable.CIBRK:
+                    shouldBreak = lastClass == LineBreakClass.Space;
+                    if (!shouldBreak)
+                    {
+                        return false;
+                    }
 
-                    case PairBreakType.CP: // prohibited for combining marks
-                        if (!lastClass.HasValue || lastClass.Value != LineBreakClass.Space)
+                    break;
+
+                case LineBreakPairTable.CPBRK: // prohibited for combining marks
+                    if (lastClass != LineBreakClass.Space)
+                    {
+                        return false;
+                    }
+
+                    break;
+
+                case LineBreakPairTable.PRBRK:
+                    break;
+            }
+
+            // Rule LB22
+            if (_nextClass == LineBreakClass.Inseparable)
+            {
+                switch (lastClass)
+                {
+                    case LineBreakClass.MandatoryBreak:
+                    case LineBreakClass.ContingentBreak:
+                    case LineBreakClass.Exclamation:
+                    case LineBreakClass.LineFeed:
+                    case LineBreakClass.NextLine:
+                    case LineBreakClass.Space:
+                    case LineBreakClass.ZWSpace:
+
+                        // Allow break
+                        break;
+                    case LineBreakClass.CombiningMark:
+                        if (_lb22ex)
                         {
-                            continue;
+                            // Allow break
+                            _lb22ex = false;
+                            break;
                         }
+
+                        shouldBreak = false;
+                        break;
+                    default:
+                        shouldBreak = false;
                         break;
                 }
+            }
 
-                _curClass = _nextClass;
+            if (_lb8a)
+            {
+                shouldBreak = false;
+            }
 
-                if (shouldBreak)
-                {
-                    Current = new LineBreak(FindPriorNonWhitespace(_lastPos), _lastPos);
-                    return true;
-                }
+            // Rule LB21a
+            if (_lb21a && (_currentClass == LineBreakClass.Hyphen || _currentClass == LineBreakClass.BreakAfter))
+            {
+                shouldBreak = false;
+                _lb21a = false;
+            }
+            else
+            {
+                _lb21a = _currentClass == LineBreakClass.HebrewLetter;
             }
 
-            if (_pos >= _text.Length)
+            // Rule LB30a
+            if (_currentClass == LineBreakClass.RegionalIndicator)
             {
-                if (_lastPos < _text.Length)
+                _lb30a++;
+                if (_lb30a == 2 && _nextClass == LineBreakClass.RegionalIndicator)
                 {
-                    _lastPos = _text.Length;
-                    var cls = Codepoint.ReadAt(_text, _text.Length - 1, out _).LineBreakClass;
-                    bool required = cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn;
-                    Current = new LineBreak(FindPriorNonWhitespace(_text.Length), _text.Length, required);
-                    return true;
+                    shouldBreak = true;
+                    _lb30a = 0;
                 }
             }
+            else
+            {
+                _lb30a = 0;
+            }
 
-            return false;
-        }
+            _currentClass = _nextClass;
 
+            return shouldBreak;
+        }
+        
         private int FindPriorNonWhitespace(int from)
         {
             if (from > 0)
@@ -163,7 +463,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
                 var cls = cp.LineBreakClass;
 
-                if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || cls == LineBreakClass.CarriageReturn)
+                if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed ||
+                    cls == LineBreakClass.CarriageReturn)
                 {
                     from -= count;
                 }
@@ -184,61 +485,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
                     break;
                 }
             }
-            return from;
-        }
 
-        // Get the next character class
-        private LineBreakClass ReadCharClass()
-        {
-            var cp = Codepoint.ReadAt(_text, _pos, out var count);
-
-            _pos += count;
-
-            return MapClass(cp.LineBreakClass);
-        }
-
-        private LineBreakClass PeekCharClass()
-        {
-            return MapClass(Codepoint.ReadAt(_text, _pos, out _).LineBreakClass);
-        }
-
-        private static LineBreakClass MapClass(LineBreakClass c)
-        {
-            switch (c)
-            {
-                case LineBreakClass.Ambiguous:
-                    return LineBreakClass.Alphabetic;
-
-                case LineBreakClass.ComplexContext:
-                case LineBreakClass.Surrogate:
-                case LineBreakClass.Unknown:
-                    return LineBreakClass.Alphabetic;
-
-                case LineBreakClass.ConditionalJapaneseStarter:
-                    return LineBreakClass.Nonstarter;
-
-                default:
-                    return c;
-            }
-        }
-
-        private static LineBreakClass MapFirst(LineBreakClass c)
-        {
-            switch (c)
-            {
-                case LineBreakClass.LineFeed:
-                case LineBreakClass.NextLine:
-                    return LineBreakClass.MandatoryBreak;
-
-                case LineBreakClass.ContingentBreak:
-                    return LineBreakClass.BreakAfter;
-
-                case LineBreakClass.Space:
-                    return LineBreakClass.WordJoiner;
-
-                default:
-                    return c;
-            }
+            return from;
         }
     }
 }

+ 74 - 0
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs

@@ -0,0 +1,74 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/
+
+namespace Avalonia.Media.TextFormatting.Unicode
+{
+    internal static class LineBreakPairTable
+    {
+        /// <summary>
+        /// Direct break opportunity
+        /// </summary>
+        public const byte DIBRK = 0;
+
+        /// <summary>
+        /// Indirect break opportunity
+        /// </summary>
+        public const byte INBRK = 1;
+
+        /// <summary>
+        /// Indirect break opportunity for combining marks
+        /// </summary>
+        public const byte CIBRK = 2;
+
+        /// <summary>
+        /// Prohibited break for combining marks
+        /// </summary>
+        public const byte CPBRK = 3;
+
+        /// <summary>
+        /// Prohibited break
+        /// </summary>
+        public const byte PRBRK = 4;
+
+        // Based on example pair table from https://www.unicode.org/reports/tr14/tr14-37.html#Table2
+        // - ZWJ special processing for LB8a
+        // - CB manually added as per Rule LB20
+        public static byte[][] Table { get; } = {
+              // .         OP     CL     CP     QU     GL     NS     EX     SY     IS     PR     PO     NU     AL     HL     ID     IN     HY     BA     BB     B2     ZW     CM     WJ     H2     H3     JL     JV     JT     RI     EB     EM     ZWJ    CB
+              new[] { PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, CPBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK, PRBRK }, // OP
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CL
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CP
+              new[] { PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // QU
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // GL
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // NS
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // EX
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, DIBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // SY
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // IS
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK }, // PR
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // PO
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // NU
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // AL
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // HL
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // ID
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // IN
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, DIBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // HY
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, DIBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // BA
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK }, // BB
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, PRBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // B2
+              new[] { DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK }, // ZW
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // CM
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK, INBRK }, // WJ
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // H2
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // H3
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JL
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JV
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // JT
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK, DIBRK, INBRK, DIBRK }, // RI
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, DIBRK }, // EB
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, DIBRK, INBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // EM
+              new[] { INBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, PRBRK, PRBRK, PRBRK, INBRK, INBRK, INBRK, INBRK, INBRK, DIBRK, INBRK, INBRK, INBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK }, // ZWJ
+              new[] { DIBRK, PRBRK, PRBRK, INBRK, INBRK, DIBRK, PRBRK, PRBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, PRBRK, CIBRK, PRBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, DIBRK, INBRK, DIBRK } // CB
+        };
+    }
+}

+ 70 - 3
src/Avalonia.X11/X11CursorFactory.cs

@@ -1,12 +1,17 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Controls.Platform.Surfaces;
 using Avalonia.Input;
 using Avalonia.Platform;
+using Avalonia.Utilities;
+
+#nullable enable
 
 namespace Avalonia.X11
 {
-    class X11CursorFactory : IStandardCursorFactory
+    class X11CursorFactory : ICursorFactory
     {
         private static readonly byte[] NullCursorData = new byte[] { 0 };
 
@@ -51,7 +56,7 @@ namespace Avalonia.X11
                 .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id));
         }
 
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        public ICursorImpl GetCursor(StandardCursorType cursorType)
         {
             IntPtr handle;
             if (cursorType == StandardCursorType.None)
@@ -64,7 +69,12 @@ namespace Avalonia.X11
                 ? _cursors[shape]
                 : _cursors[CursorFontShape.XC_top_left_arrow];
             }
-            return new PlatformHandle(handle, "XCURSOR");
+            return new CursorImpl(handle);
+        }
+
+        public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
+        {
+            return new XImageCursor(_display, cursor, hotSpot);
         }
 
         private static IntPtr GetNullCursor(IntPtr display)
@@ -74,5 +84,62 @@ namespace Avalonia.X11
             IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, NullCursorData, 1, 1);
             return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0);
         }
+
+        private unsafe class XImageCursor : CursorImpl, IFramebufferPlatformSurface, IPlatformHandle
+        {
+            private readonly PixelSize _pixelSize;
+            private readonly IUnmanagedBlob _blob;
+
+            public XImageCursor(IntPtr display, IBitmapImpl bitmap, PixelPoint hotSpot)
+            {
+                var size = Marshal.SizeOf<XcursorImage>() +
+                    (bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4);
+
+                _pixelSize = bitmap.PixelSize;
+                _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(size);
+                
+                var image = (XcursorImage*)_blob.Address;
+                image->version = 1;
+                image->size = Marshal.SizeOf<XcursorImage>();
+                image->width = bitmap.PixelSize.Width;
+                image->height = bitmap.PixelSize.Height;
+                image->xhot = hotSpot.X;
+                image->yhot = hotSpot.Y;
+                image->pixels = (IntPtr)(image + 1);
+               
+                using (var renderTarget = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().CreateRenderTarget(new[] { this }))
+                using (var ctx = renderTarget.CreateDrawingContext(null))
+                {
+                    var r = new Rect(_pixelSize.ToSize(1)); 
+                    ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r);
+                }
+
+                Handle = XLib.XcursorImageLoadCursor(display, _blob.Address);
+            }
+
+            public string HandleDescriptor => "XCURSOR";
+
+            public override void Dispose()
+            {
+                XLib.XcursorImageDestroy(Handle);
+                _blob.Dispose();
+            }
+
+            public ILockedFramebuffer Lock()
+            {
+                return new LockedFramebuffer(
+                    _blob.Address + Marshal.SizeOf<XcursorImage>(),
+                    _pixelSize, _pixelSize.Width * 4,
+                    new Vector(96, 96), PixelFormat.Bgra8888, null);
+            }
+        }
+    }
+
+    class CursorImpl : ICursorImpl
+    {
+        public CursorImpl() { }
+        public CursorImpl(IntPtr handle) => Handle = handle;
+        public IntPtr Handle { get; protected set; }
+        public virtual void Dispose() { }
     }
 }

+ 1 - 1
src/Avalonia.X11/X11Platform.cs

@@ -71,7 +71,7 @@ namespace Avalonia.X11
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
                 .Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
-                .Bind<IStandardCursorFactory>().ToConstant(new X11CursorFactory(Display))
+                .Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
                 .Bind<IClipboard>().ToConstant(new X11Clipboard(this))
                 .Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
                 .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))

+ 1 - 1
src/Avalonia.X11/X11Structs.cs

@@ -1693,7 +1693,7 @@ namespace Avalonia.X11 {
 	[StructLayout (LayoutKind.Sequential)]
 	internal struct XcursorImage
 	{
-		private int version;
+		public int version;
 		public int size;       /* nominal size for matching */
 		public int width;      /* actual width */
 		public int height;     /* actual height */

+ 2 - 2
src/Avalonia.X11/X11Window.Ime.cs

@@ -96,14 +96,14 @@ namespace Avalonia.X11
 
         void HandleKeyEvent(ref XEvent ev)
         {
-            var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
+            var index = ev.KeyEvent.state.HasFlagCustom(XModifierMask.ShiftMask);
 
             // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
             var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
                 
             // Manually switch the Shift index for the keypad,
             // there should be a proper way to do this
-            if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
+            if (ev.KeyEvent.state.HasFlagCustom(XModifierMask.Mod2Mask)
                 && key > X11Key.Num_Lock && key <= X11Key.KP_9)
                 key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
             

+ 12 - 14
src/Avalonia.X11/X11Window.cs

@@ -639,23 +639,23 @@ namespace Avalonia.X11
         RawInputModifiers TranslateModifiers(XModifierMask state)
         {
             var rv = default(RawInputModifiers);
-            if (state.HasFlag(XModifierMask.Button1Mask))
+            if (state.HasFlagCustom(XModifierMask.Button1Mask))
                 rv |= RawInputModifiers.LeftMouseButton;
-            if (state.HasFlag(XModifierMask.Button2Mask))
+            if (state.HasFlagCustom(XModifierMask.Button2Mask))
                 rv |= RawInputModifiers.RightMouseButton;
-            if (state.HasFlag(XModifierMask.Button3Mask))
+            if (state.HasFlagCustom(XModifierMask.Button3Mask))
                 rv |= RawInputModifiers.MiddleMouseButton;
-            if (state.HasFlag(XModifierMask.Button4Mask))
+            if (state.HasFlagCustom(XModifierMask.Button4Mask))
                 rv |= RawInputModifiers.XButton1MouseButton;
-            if (state.HasFlag(XModifierMask.Button5Mask))
+            if (state.HasFlagCustom(XModifierMask.Button5Mask))
                 rv |= RawInputModifiers.XButton2MouseButton;
-            if (state.HasFlag(XModifierMask.ShiftMask))
+            if (state.HasFlagCustom(XModifierMask.ShiftMask))
                 rv |= RawInputModifiers.Shift;
-            if (state.HasFlag(XModifierMask.ControlMask))
+            if (state.HasFlagCustom(XModifierMask.ControlMask))
                 rv |= RawInputModifiers.Control;
-            if (state.HasFlag(XModifierMask.Mod1Mask))
+            if (state.HasFlagCustom(XModifierMask.Mod1Mask))
                 rv |= RawInputModifiers.Alt;
-            if (state.HasFlag(XModifierMask.Mod4Mask))
+            if (state.HasFlagCustom(XModifierMask.Mod4Mask))
                 rv |= RawInputModifiers.Meta;
             return rv;
         }
@@ -872,15 +872,13 @@ namespace Avalonia.X11
             UpdateSizeHints(null);
         }
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
             if (cursor == null)
                 XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor);
-            else
+            else if (cursor is CursorImpl impl)
             {
-                if (cursor.HandleDescriptor != "XCURSOR")
-                    throw new ArgumentException("Expected XCURSOR handle type");
-                XDefineCursor(_x11.Display, _handle, cursor.Handle);
+                XDefineCursor(_x11.Display, _handle, impl.Handle);
             }
         }
 

+ 5 - 5
src/Avalonia.X11/XI2Manager.cs

@@ -342,13 +342,13 @@ namespace Avalonia.X11
             Type = ev->evtype;
             Timestamp = (ulong)ev->time.ToInt64();
             var state = (XModifierMask)ev->mods.Effective;
-            if (state.HasFlag(XModifierMask.ShiftMask))
+            if (state.HasFlagCustom(XModifierMask.ShiftMask))
                 Modifiers |= RawInputModifiers.Shift;
-            if (state.HasFlag(XModifierMask.ControlMask))
+            if (state.HasFlagCustom(XModifierMask.ControlMask))
                 Modifiers |= RawInputModifiers.Control;
-            if (state.HasFlag(XModifierMask.Mod1Mask))
+            if (state.HasFlagCustom(XModifierMask.Mod1Mask))
                 Modifiers |= RawInputModifiers.Alt;
-            if (state.HasFlag(XModifierMask.Mod4Mask))
+            if (state.HasFlagCustom(XModifierMask.Mod4Mask))
                 Modifiers |= RawInputModifiers.Meta;
 
             Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask);
@@ -364,7 +364,7 @@ namespace Avalonia.X11
             if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
                 Button = ev->detail;
             Detail = ev->detail;
-            Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated);
+            Emulated = ev->flags.HasFlagCustom(XiDeviceEventFlags.XIPointerEmulated);
         }
     }
     

+ 7 - 0
src/Avalonia.X11/XLib.cs

@@ -20,6 +20,7 @@ namespace Avalonia.X11
         const string libX11Randr = "libXrandr.so.2";
         const string libX11Ext = "libXext.so.6";
         const string libXInput = "libXi.so.6";
+        const string libXCursor = "libXcursor.so.1";
 
         [DllImport(libX11)]
         public static extern IntPtr XOpenDisplay(IntPtr display);
@@ -569,6 +570,12 @@ namespace Avalonia.X11
         [DllImport(libXInput)]
         public static extern void XIFreeDeviceInfo(XIDeviceInfo* info);
 
+        [DllImport(libXCursor)]
+        public static extern IntPtr XcursorImageLoadCursor(IntPtr display, IntPtr image);
+
+        [DllImport(libXCursor)]
+        public static extern IntPtr XcursorImageDestroy(IntPtr image);
+
         public static void XISetMask(ref int mask, XiEventType ev)
         {
             mask |= (1 << (int)ev);

+ 1 - 1
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -57,7 +57,7 @@ namespace Avalonia.LinuxFramebuffer
 
         public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1);
 
-        public void SetCursor(IPlatformHandle cursor)
+        public void SetCursor(ICursorImpl cursor)
         {
         }
 

+ 7 - 2
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Diagnostics;
 using System.Threading;
 using Avalonia.Controls;
@@ -38,7 +38,7 @@ namespace Avalonia.LinuxFramebuffer
                 .Bind<IPlatformThreadingInterface>().ToConstant(Threading)
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
-                .Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
+                .Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
@@ -79,6 +79,11 @@ namespace Avalonia.LinuxFramebuffer
                     tl.Prepare();
                     _topLevel = tl;
                     _topLevel.Renderer.Start();
+
+                    if (_topLevel is IFocusScope scope)
+                    {
+                        FocusManager.Instance?.SetFocusScope(scope);
+                    }
                 }
 
                 _topLevel.Content = value;

+ 1 - 1
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs

@@ -54,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer.Output
         }
 
         public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay);
-        public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED);
+        public bool IsPreferred => Mode.type.HasFlagCustom(DrmModeType.DRM_MODE_TYPE_PREFERRED);
 
         public string Name { get; }
     }

+ 6 - 3
src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs

@@ -4,11 +4,14 @@ using Avalonia.Platform;
 
 namespace Avalonia.LinuxFramebuffer
 {
-    internal class CursorFactoryStub : IStandardCursorFactory
+    internal class CursorFactoryStub : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
+
+        private class CursorStub : ICursorImpl
         {
-            return new PlatformHandle(IntPtr.Zero, null);
+            public void Dispose() { }
         }
     }
     internal class PlatformSettings : IPlatformSettings

+ 9 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@@ -198,6 +198,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result);
             }
 
+            if (type.Equals(types.Classes))
+            {
+                var classes = text.Split(' ');
+                var classNodes = classes.Select(c => new XamlAstTextNode(node, c, types.XamlIlTypes.String)).ToArray();
+
+                result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes);
+                return true;
+            }
+
             result = null;
             return false;
         }

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -77,6 +77,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType RowDefinitions { get; }
         public IXamlType ColumnDefinition { get; }
         public IXamlType ColumnDefinitions { get; }
+        public IXamlType Classes { get; }
 
         public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
         {
@@ -166,6 +167,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             ColumnDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.ColumnDefinitions");
             RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition");
             RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions");
+            Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes");
         }
     }
 

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit ea80a607c5e9d8f000160dbbb48c27ed4cfafbc9
+Subproject commit f3ca2028f4f64be3556a6afd22f192902de095e5

+ 7 - 4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

@@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
         internal object RawSource { get; }
 
         public override string ToString()
-            => string.Concat(_elements.Select(e => e.ToString()));
+            => string.Concat(_elements);
     }
 
     public class CompiledBindingPathBuilder
@@ -88,7 +88,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
 
         public CompiledBindingPathBuilder Property(IPropertyInfo info, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory)
         {
-            _elements.Add(new PropertyElement(info, accessorFactory));
+            _elements.Add(new PropertyElement(info, accessorFactory, _elements.Count == 0));
             return this;
         }
 
@@ -161,10 +161,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
 
     internal class PropertyElement : ICompiledBindingPathElement
     {
-        public PropertyElement(IPropertyInfo property, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory)
+        private readonly bool _isFirstElement;
+
+        public PropertyElement(IPropertyInfo property, Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> accessorFactory, bool isFirstElement)
         {
             Property = property;
             AccessorFactory = accessorFactory;
+            _isFirstElement = isFirstElement;
         }
 
         public IPropertyInfo Property { get; }
@@ -172,7 +175,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
         public Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; }
 
         public override string ToString()
-            => $".{Property.Name}";
+            => _isFirstElement ? Property.Name : $".{Property.Name}";
     }
 
     internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement

+ 2 - 0
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -2,6 +2,8 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <RootNamespace>Avalonia</RootNamespace>
+    <Nullable>Enable</Nullable>
+    <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
   </PropertyGroup>
   <ItemGroup>
     <None Remove="Markup\Parsers\Nodes\ExpressionGrammer" />

+ 26 - 23
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -39,17 +39,17 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets the name of the element to use as the binding source.
         /// </summary>
-        public string ElementName { get; set; }
+        public string? ElementName { get; set; }
 
         /// <summary>
         /// Gets or sets the relative source for the binding.
         /// </summary>
-        public RelativeSource RelativeSource { get; set; }
+        public RelativeSource? RelativeSource { get; set; }
 
         /// <summary>
         /// Gets or sets the source for the binding.
         /// </summary>
-        public object Source { get; set; }
+        public object? Source { get; set; }
 
         /// <summary>
         /// Gets or sets the binding path.
@@ -59,24 +59,36 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets a function used to resolve types from names in the binding path.
         /// </summary>
-        public Func<string, string, Type> TypeResolver { get; set; }
+        public Func<string, string, Type>? TypeResolver { get; set; }
 
-        protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation)
+        protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object? anchor, bool enableDataValidation)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
-            anchor = anchor ?? DefaultAnchor?.Target;
-            
+            _ = target ?? throw new ArgumentNullException(nameof(target));
+
+            anchor ??= DefaultAnchor?.Target;
             enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
 
-            INameScope nameScope = null;
+            INameScope? nameScope = null;
             NameScope?.TryGetTarget(out nameScope);
 
             var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope);
 
+            if (node is null)
+            {
+                throw new InvalidOperationException("Could not parse binding expression.");
+            }
+
+            IStyledElement GetSource()
+            {
+                return target as IStyledElement ??
+                    anchor as IStyledElement ??
+                    throw new ArgumentException("Could not find binding source: either target or anchor must be an IStyledElement.");
+            }
+
             if (ElementName != null)
             {
                 return CreateElementObserver(
-                    (target as IStyledElement) ?? (anchor as IStyledElement),
+                    GetSource(),
                     ElementName,
                     node);
             }
@@ -96,9 +108,7 @@ namespace Avalonia.Data
                 }
                 else
                 {
-                    return CreateSourceObserver(
-                        (target as IStyledElement) ?? (anchor as IStyledElement),
-                        node);
+                    return CreateSourceObserver(GetSource(), node);
                 }
             }
             else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
@@ -111,15 +121,11 @@ namespace Avalonia.Data
             }
             else if (RelativeSource.Mode == RelativeSourceMode.Self)
             {
-                return CreateSourceObserver(
-                    (target as IStyledElement) ?? (anchor as IStyledElement),
-                    node);
+                return CreateSourceObserver(GetSource(), node);
             }
             else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
             {
-                return CreateTemplatedParentObserver(
-                    (target as IStyledElement) ?? (anchor as IStyledElement),
-                    node);
+                return CreateTemplatedParentObserver(GetSource(), node);
             }
             else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
             {
@@ -128,10 +134,7 @@ namespace Avalonia.Data
                     throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.");
                 }
 
-                return CreateFindAncestorObserver(
-                    (target as IStyledElement) ?? (anchor as IStyledElement),
-                    RelativeSource,
-                    node);
+                return CreateFindAncestorObserver(GetSource(), RelativeSource, node);
             }
             else
             {

+ 22 - 27
src/Markup/Avalonia.Markup/Data/BindingBase.cs

@@ -38,22 +38,22 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets the <see cref="IValueConverter"/> to use.
         /// </summary>
-        public IValueConverter Converter { get; set; }
+        public IValueConverter? Converter { get; set; }
 
         /// <summary>
         /// Gets or sets a parameter to pass to <see cref="Converter"/>.
         /// </summary>
-        public object ConverterParameter { get; set; }
+        public object? ConverterParameter { get; set; }
 
         /// <summary>
         /// Gets or sets the value to use when the binding is unable to produce a value.
         /// </summary>
-        public object FallbackValue { get; set; }
+        public object? FallbackValue { get; set; }
 
         /// <summary>
         /// Gets or sets the value to use when the binding result is null.
         /// </summary>
-        public object TargetNullValue { get; set; }
+        public object? TargetNullValue { get; set; }
 
         /// <summary>
         /// Gets or sets the binding mode.
@@ -68,26 +68,27 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets the string format.
         /// </summary>
-        public string StringFormat { get; set; }
+        public string? StringFormat { get; set; }
 
-        public WeakReference DefaultAnchor { get; set; }
+        public WeakReference? DefaultAnchor { get; set; }
 
-        public WeakReference<INameScope> NameScope { get; set; }
+        public WeakReference<INameScope>? NameScope { get; set; }
 
         protected abstract ExpressionObserver CreateExpressionObserver(
             IAvaloniaObject target,
             AvaloniaProperty targetProperty,
-            object anchor,
+            object? anchor,
             bool enableDataValidation);
 
         /// <inheritdoc/>
         public InstancedBinding Initiate(
             IAvaloniaObject target,
             AvaloniaProperty targetProperty,
-            object anchor = null,
+            object? anchor = null,
             bool enableDataValidation = false)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
+            _ = target ?? throw new ArgumentNullException(nameof(target));
+
             anchor = anchor ?? DefaultAnchor?.Target;
 
             enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
@@ -133,18 +134,13 @@ namespace Avalonia.Data
             IAvaloniaObject target,
             ExpressionNode node,
             bool targetIsDataContext,
-            object anchor)
+            object? anchor)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
+            _ = target ?? throw new ArgumentNullException(nameof(target));
 
             if (!(target is IDataContextProvider))
             {
-                target = anchor as IDataContextProvider;
-
-                if (target == null)
-                {
-                    throw new InvalidOperationException("Cannot find a DataContext to bind to.");
-                }
+                target = anchor as IDataContextProvider ?? throw new InvalidOperationException("Cannot find a DataContext to bind to.");
             }
 
             if (!targetIsDataContext)
@@ -171,10 +167,9 @@ namespace Avalonia.Data
             string elementName,
             ExpressionNode node)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
+            _ = target ?? throw new ArgumentNullException(nameof(target));
 
-            NameScope.TryGetTarget(out var scope);
-            if (scope == null)
+            if (NameScope is null || !NameScope.TryGetTarget(out var scope) || scope is null)
                 throw new InvalidOperationException("Name scope is null or was already collected");
             var result = new ExpressionObserver(
                 NameScopeLocator.Track(scope, elementName),
@@ -188,9 +183,9 @@ namespace Avalonia.Data
             RelativeSource relativeSource,
             ExpressionNode node)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
+            _ = target ?? throw new ArgumentNullException(nameof(target));
 
-            IObservable<object> controlLocator;
+            IObservable<object?> controlLocator;
 
             switch (relativeSource.Tree)
             {
@@ -220,7 +215,7 @@ namespace Avalonia.Data
             object source,
             ExpressionNode node)
         {
-            Contract.Requires<ArgumentNullException>(source != null);
+            _ = source ?? throw new ArgumentNullException(nameof(source));
 
             return new ExpressionObserver(source, node);
         }
@@ -229,7 +224,7 @@ namespace Avalonia.Data
             IAvaloniaObject target,
             ExpressionNode node)
         {
-            Contract.Requires<ArgumentNullException>(target != null);
+            _ = target ?? throw new ArgumentNullException(nameof(target));
 
             var result = new ExpressionObserver(
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
@@ -240,7 +235,7 @@ namespace Avalonia.Data
             return result;
         }
 
-        protected IObservable<object> GetParentDataContext(IAvaloniaObject target)
+        protected IObservable<object?> GetParentDataContext(IAvaloniaObject target)
         {
             // The DataContext is based on the visual parent and not the logical parent: this may
             // seem counter intuitive considering the fact that property inheritance works on the logical
@@ -252,7 +247,7 @@ namespace Avalonia.Data
                 .Select(x =>
                 {
                     return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ??
-                           Observable.Return((object)null);
+                           Observable.Return((object?)null);
                 }).Switch();
         }
 

+ 7 - 7
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@@ -22,12 +22,12 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets the <see cref="IMultiValueConverter"/> to use.
         /// </summary>
-        public IMultiValueConverter Converter { get; set; }
+        public IMultiValueConverter? Converter { get; set; }
 
         /// <summary>
         /// Gets or sets a parameter to pass to <see cref="Converter"/>.
         /// </summary>
-        public object ConverterParameter { get; set; }
+        public object? ConverterParameter { get; set; }
 
         /// <summary>
         /// Gets or sets the value to use when the binding is unable to produce a value.
@@ -52,12 +52,12 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets or sets the relative source for the binding.
         /// </summary>
-        public RelativeSource RelativeSource { get; set; }
+        public RelativeSource? RelativeSource { get; set; }
 
         /// <summary>
         /// Gets or sets the string format.
         /// </summary>
-        public string StringFormat { get; set; }
+        public string? StringFormat { get; set; }
 
         public MultiBinding()
         {
@@ -69,7 +69,7 @@ namespace Avalonia.Data
         public InstancedBinding Initiate(
             IAvaloniaObject target,
             AvaloniaProperty targetProperty,
-            object anchor = null,
+            object? anchor = null,
             bool enableDataValidation = false)
         {
             var targetType = targetProperty?.PropertyType ?? typeof(object);
@@ -105,7 +105,7 @@ namespace Avalonia.Data
             }
         }
 
-        private object ConvertValue(IList<object> values, Type targetType, IMultiValueConverter converter)
+        private object ConvertValue(IList<object?> values, Type targetType, IMultiValueConverter? converter)
         {
             for (var i = 0; i < values.Count; ++i)
             {
@@ -116,7 +116,7 @@ namespace Avalonia.Data
             }
 
             var culture = CultureInfo.CurrentCulture;
-            values = new System.Collections.ObjectModel.ReadOnlyCollection<object>(values);
+            values = new System.Collections.ObjectModel.ReadOnlyCollection<object?>(values);
             object converted;
             if (converter != null)
             {

Some files were not shown because too many files changed in this diff