Browse Source

Merge branch 'master' into dotnet-core-sdk-update

jp2masa 6 years ago
parent
commit
1f4c9dc3da
100 changed files with 2605 additions and 677 deletions
  1. 2 1
      Avalonia.sln
  2. 1 1
      azure-pipelines.yml
  3. 6 0
      build/HarfBuzzSharp.props
  4. 2 2
      build/SkiaSharp.props
  5. 9 2
      native/Avalonia.Native/src/OSX/window.mm
  6. 0 12
      samples/ControlCatalog/App.xaml
  7. 0 11
      samples/ControlCatalog/App.xaml.cs
  8. 16 8
      samples/ControlCatalog/MainWindow.xaml
  9. 11 3
      samples/ControlCatalog/MainWindow.xaml.cs
  10. 2 1
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  11. 9 0
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  12. 28 8
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  13. 20 0
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  14. 1 0
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  15. 1 1
      src/Avalonia.Animation/TransitionInstance.cs
  16. 9 2
      src/Avalonia.Base/AvaloniaObject.cs
  17. 41 22
      src/Avalonia.Base/AvaloniaProperty.cs
  18. 23 0
      src/Avalonia.Base/EnumExtensions.cs
  19. 5 0
      src/Avalonia.Base/Utilities/MathUtilities.cs
  20. 6 6
      src/Avalonia.Base/ValueStore.cs
  21. 2 3
      src/Avalonia.Controls/AppBuilderBase.cs
  22. 8 0
      src/Avalonia.Controls/Application.cs
  23. 4 5
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  24. 1 1
      src/Avalonia.Controls/AutoCompleteBox.cs
  25. 10 3
      src/Avalonia.Controls/ColumnDefinition.cs
  26. 51 3
      src/Avalonia.Controls/DefinitionBase.cs
  27. 1 0
      src/Avalonia.Controls/DesktopApplicationExtensions.cs
  28. 0 7
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  29. 12 0
      src/Avalonia.Controls/Grid.cs
  30. 749 118
      src/Avalonia.Controls/GridSplitter.cs
  31. 9 1
      src/Avalonia.Controls/ItemsControl.cs
  32. 14 8
      src/Avalonia.Controls/ListBox.cs
  33. 2 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  34. 7 0
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  35. 13 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  36. 2 1
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  37. 2 0
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  38. 27 12
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  39. 8 2
      src/Avalonia.Controls/Primitives/TabStrip.cs
  40. 0 2
      src/Avalonia.Controls/Primitives/Thumb.cs
  41. 11 1
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  42. 10 3
      src/Avalonia.Controls/RowDefinition.cs
  43. 3 12
      src/Avalonia.Controls/Shapes/Shape.cs
  44. 56 1
      src/Avalonia.Controls/TabControl.cs
  45. 0 21
      src/Avalonia.Controls/TabItem.cs
  46. 3 2
      src/Avalonia.Controls/TextBlock.cs
  47. 4 4
      src/Avalonia.Controls/TextBox.cs
  48. 5 1
      src/Avalonia.Controls/ToolTipService.cs
  49. 12 7
      src/Avalonia.Controls/TreeView.cs
  50. 1 1
      src/Avalonia.Controls/WrapPanel.cs
  51. 2 2
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  52. 105 0
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  53. 62 0
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  54. BIN
      src/Avalonia.Dialogs/Assets/Roboto-Light.ttf
  55. 1 0
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  56. 6 1
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  57. 1 1
      src/Avalonia.Input/AccessKeyHandler.cs
  58. 1 1
      src/Avalonia.Input/FocusManager.cs
  59. 11 2
      src/Avalonia.Input/MouseDevice.cs
  60. 1 1
      src/Avalonia.Input/Pointer.cs
  61. 6 3
      src/Avalonia.Input/PointerPoint.cs
  62. 18 3
      src/Avalonia.Input/TouchDevice.cs
  63. 5 2
      src/Avalonia.Layout/LayoutHelper.cs
  64. 7 1
      src/Avalonia.Layout/LayoutManager.cs
  65. 17 8
      src/Avalonia.Layout/LayoutQueue.cs
  66. 39 12
      src/Avalonia.Layout/Layoutable.cs
  67. 1 0
      src/Avalonia.Native/Avalonia.Native.csproj
  68. 32 2
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  69. 0 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  70. 4 3
      src/Avalonia.Native/WindowImplBase.cs
  71. 15 43
      src/Avalonia.Themes.Default/GridSplitter.xaml
  72. 20 8
      src/Avalonia.Visuals/Media/FontFamily.cs
  73. 112 0
      src/Avalonia.Visuals/Media/FontManager.cs
  74. 5 1
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  75. 42 5
      src/Avalonia.Visuals/Media/FormattedText.cs
  76. 111 0
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  77. 72 27
      src/Avalonia.Visuals/Media/Typeface.cs
  78. 48 0
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  79. 89 0
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  80. 11 5
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  81. 4 2
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  82. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  83. 0 2
      src/Avalonia.X11/X11Platform.cs
  84. 8 2
      src/Avalonia.X11/X11Window.cs
  85. 6 6
      src/Avalonia.X11/XI2Manager.cs
  86. 1 0
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  87. 40 0
      src/Skia/Avalonia.Skia/FontKey.cs
  88. 82 0
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  89. 38 42
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  90. 179 0
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  91. 11 3
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  92. 18 73
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  93. 6 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  94. 5 0
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  95. 17 69
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  96. 19 0
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  97. 1 0
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  98. 11 14
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  99. 24 25
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  100. 71 0
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

+ 2 - 1
Avalonia.sln

@@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\Base.props = build\Base.props
 		build\Binding.props = build\Binding.props
 		build\BuildTargets.targets = build\BuildTargets.targets
+		build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
 		build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
 		build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
 		build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
 EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution

+ 1 - 1
azure-pipelines.yml

@@ -60,7 +60,7 @@ jobs:
       sdk: 'macosx10.14'
       configuration: 'Release'
       xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
-      xcodeVersion: 'default' # Options: 8, 9, default, specifyPath
+      xcodeVersion: '10' # Options: 8, 9, default, specifyPath
       args: '-derivedDataPath ./'
 
   - task: CmdLine@2

+ 6 - 0
build/HarfBuzzSharp.props

@@ -0,0 +1,6 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <PackageReference Include="HarfBuzzSharp" Version="2.6.1-rc.153" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1-rc.153" />
+  </ItemGroup>
+</Project>

+ 2 - 2
build/SkiaSharp.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="1.68.0" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
+    <PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1-rc.153" />
   </ItemGroup>
 </Project>

+ 9 - 2
native/Avalonia.Native/src/OSX/window.mm

@@ -855,8 +855,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
     if(type == Wheel)
     {
-        delta.X = [event scrollingDeltaX] / 50;
-        delta.Y = [event scrollingDeltaY] / 50;
+        auto speed = 5;
+        
+        if([event hasPreciseScrollingDeltas])
+        {
+            speed = 50;
+        }
+        
+        delta.X = [event scrollingDeltaX] / speed;
+        delta.Y = [event scrollingDeltaY] / speed;
         
         if(delta.X == 0 && delta.Y == 0)
         {

+ 0 - 12
samples/ControlCatalog/App.xaml

@@ -17,16 +17,4 @@
     </Style>
     <StyleInclude Source="/SideBar.xaml"/>
   </Application.Styles>
-
-  <NativeMenu.Menu>
-      <NativeMenu>
-        <NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
-        <NativeMenuItem Header="Recent">
-          <NativeMenuItem.Menu>
-            <NativeMenu/>
-          </NativeMenuItem.Menu>
-        </NativeMenuItem>
-        <NativeMenuItem Header="Quit Avalonia" Gesture="CMD+Q"/>
-      </NativeMenu>
-  </NativeMenu.Menu>
 </Application>

+ 0 - 11
samples/ControlCatalog/App.xaml.cs

@@ -8,20 +8,9 @@ namespace ControlCatalog
 {
     public class App : Application
     {
-        private NativeMenu _recentMenu;
-
         public override void Initialize()
         {
             AvaloniaXamlLoader.Load(this);
-
-            Name = "Avalonia";
-
-            _recentMenu = (NativeMenu.GetMenu(this).Items[1] as NativeMenuItem).Menu;
-        }
-
-        public void OnOpenClicked(object sender, EventArgs args)
-        {
-            _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
         }
 
         public override void OnFrameworkInitializationCompleted()

+ 16 - 8
samples/ControlCatalog/MainWindow.xaml

@@ -37,12 +37,20 @@
     </NativeMenu>
   </NativeMenu.Menu>
 
-  <Window.DataTemplates>
-        <DataTemplate DataType="vm:NotificationViewModel">
-            <v:CustomNotificationView />
-        </DataTemplate>
-    </Window.DataTemplates>
-    <Panel>
-        <local:MainView/>
-    </Panel>
+ <Window.DataTemplates>
+    <DataTemplate DataType="vm:NotificationViewModel">
+      <v:CustomNotificationView />
+    </DataTemplate>
+  </Window.DataTemplates>
+  <DockPanel LastChildFill="True">
+    <Menu Name="MainMenu" DockPanel.Dock="Top">
+      <MenuItem Header="File">
+        <MenuItem Header="Exit" Command="{Binding ExitCommand}" />
+      </MenuItem>
+      <MenuItem Header="Help">
+        <MenuItem Header="About" Command="{Binding AboutCommand}" />
+      </MenuItem>
+    </Menu>
+    <local:MainView />
+  </DockPanel>
 </Window>

+ 11 - 3
samples/ControlCatalog/MainWindow.xaml.cs

@@ -31,20 +31,28 @@ namespace ControlCatalog
 
             DataContext = new MainWindowViewModel(_notificationArea);
             _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
+            var mainMenu = this.FindControl<Menu>("MainMenu");
+            mainMenu.AttachedToVisualTree += MenuAttached;
+        }
+
+        public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
+        {
+            if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)
+            {
+                mainMenu.IsVisible = false;
+            }
         }
 
         public void OnOpenClicked(object sender, EventArgs args)
         {
             _recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
         }
-        
+
         public void OnCloseClicked(object sender, EventArgs args)
         {
             Close();
         }
 
-
-
         private void InitializeComponent()
         {
             // TODO: iOS does not support dynamically loading assemblies

+ 2 - 1
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -15,6 +15,7 @@
       </ComboBox>
       <Button Command="{Binding AddItem}">Add Item</Button>
       <Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
+      <Button Command="{Binding ResetItems}">Reset items</Button>
     </StackPanel>
     <Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
       <ScrollViewer Name="scroller"
@@ -23,7 +24,7 @@
         <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
           <ItemsRepeater.ItemTemplate>
             <DataTemplate>
-              <TextBlock Height="{Binding Height}" Text="{Binding Text}"/>
+              <TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}"/>
             </DataTemplate>
           </ItemsRepeater.ItemTemplate>
         </ItemsRepeater>

+ 9 - 0
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@@ -20,6 +20,7 @@ namespace ControlCatalog.Pages
             _repeater = this.FindControl<ItemsRepeater>("repeater");
             _scroller = this.FindControl<ScrollViewer>("scroller");
             _repeater.PointerPressed += RepeaterClick;
+            _repeater.KeyDown += RepeaterOnKeyDown;
             DataContext = new ItemsRepeaterPageViewModel();
         }
 
@@ -77,5 +78,13 @@ namespace ControlCatalog.Pages
             var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
             ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
         }
+
+        private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.F5)
+            {
+                ((ItemsRepeaterPageViewModel)DataContext).ResetItems();
+            }
+        }
     }
 }

+ 28 - 8
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@@ -7,25 +7,27 @@ namespace ControlCatalog.ViewModels
 {
     public class ItemsRepeaterPageViewModel : ReactiveObject
     {
-        private int newItemIndex = 1;
+        private int _newItemIndex = 1;
+        private int _newGenerationIndex = 0;
+        private ObservableCollection<Item> _items;
 
         public ItemsRepeaterPageViewModel()
         {
-            Items = new ObservableCollection<Item>(
-                Enumerable.Range(1, 100000).Select(i => new Item
-                {
-                    Text = $"Item {i.ToString()}",
-                }));
+            Items = CreateItems();
         }
 
-        public ObservableCollection<Item> Items { get; }
+        public ObservableCollection<Item> Items
+        {
+            get => _items;
+            set => this.RaiseAndSetIfChanged(ref _items, value);
+        }
 
         public Item SelectedItem { get; set; }
 
         public void AddItem()
         {
             var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
-            Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" });
+            Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" });
         }
 
         public void RandomizeHeights()
@@ -38,6 +40,24 @@ namespace ControlCatalog.ViewModels
             }
         }
 
+        public void ResetItems()
+        {
+            Items = CreateItems();
+        }
+
+        private ObservableCollection<Item> CreateItems()
+        {
+            var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]";
+
+            _newGenerationIndex++;
+
+            return new ObservableCollection<Item>(
+                Enumerable.Range(1, 100000).Select(i => new Item
+                {
+                    Text = $"Item {i.ToString()} {suffix}"
+                }));
+        }
+
         public class Item : ReactiveObject
         {
             private double _height = double.NaN;

+ 20 - 0
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -1,5 +1,7 @@
 using System.Reactive;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Notifications;
+using Avalonia.Dialogs;
 using ReactiveUI;
 
 namespace ControlCatalog.ViewModels
@@ -26,6 +28,20 @@ namespace ControlCatalog.ViewModels
             {
                 NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error));
             });
+
+            AboutCommand = ReactiveCommand.CreateFromTask(async () =>
+            {
+                var dialog = new AboutAvaloniaDialog();
+
+                var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+
+                await dialog.ShowDialog(mainWindow);
+            });
+
+            ExitCommand = ReactiveCommand.Create(() =>
+            {
+                (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
+            });
         }
 
         public IManagedNotificationManager NotificationManager
@@ -39,5 +55,9 @@ namespace ControlCatalog.ViewModels
         public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; }
 
         public ReactiveCommand<Unit, Unit> ShowNativeNotificationCommand { get; }
+
+        public ReactiveCommand<Unit, Unit> AboutCommand { get; }
+
+        public ReactiveCommand<Unit, Unit> ExitCommand { get; }
     }
 }

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

@@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
 
 [assembly: InternalsVisibleTo("Avalonia.LeakTests")]
+[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]

+ 1 - 1
src/Avalonia.Animation/TransitionInstance.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Animation
 
         private void TimerTick(TimeSpan t)
         {
-            var interpVal = (double)t.Ticks / _duration.Ticks;
+            var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
 
             // Clamp interpolation value.
             if (interpVal >= 1d | interpVal < 0d)

+ 9 - 2
src/Avalonia.Base/AvaloniaObject.cs

@@ -210,7 +210,11 @@ namespace Avalonia
         /// <returns>The value.</returns>
         public object GetValue(AvaloniaProperty property)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            if (property is null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
+
             VerifyAccess();
 
             if (property.IsDirect)
@@ -231,7 +235,10 @@ namespace Avalonia
         /// <returns>The value.</returns>
         public T GetValue<T>(AvaloniaProperty<T> property)
         {
-            Contract.Requires<ArgumentNullException>(property != null);
+            if (property is null)
+            {
+                throw new ArgumentNullException(nameof(property));
+            }
 
             return (T)GetValue((AvaloniaProperty)property);
         }

+ 41 - 22
src/Avalonia.Base/AvaloniaProperty.cs

@@ -28,6 +28,8 @@ namespace Avalonia
         private readonly Dictionary<Type, PropertyMetadata> _metadata;
         private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>();
 
+        private bool _hasMetadataOverrides;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
         /// </summary>
@@ -92,6 +94,9 @@ namespace Avalonia
             Id = source.Id;
             _defaultMetadata = source._defaultMetadata;
 
+            // Properties that have different owner can't use fast path for metadata.
+            _hasMetadataOverrides = true;
+
             if (metadata != null)
             {
                 _metadata.Add(ownerType, metadata);
@@ -446,31 +451,12 @@ namespace Avalonia
         ///
         public PropertyMetadata GetMetadata(Type type)
         {
-            Contract.Requires<ArgumentNullException>(type != null);
-
-            PropertyMetadata result;
-            Type currentType = type;
-
-            if (_metadataCache.TryGetValue(type, out result))
+            if (!_hasMetadataOverrides)
             {
-                return result;
+                return _defaultMetadata;
             }
 
-            while (currentType != null)
-            {
-                if (_metadata.TryGetValue(currentType, out result))
-                {
-                    _metadataCache[type] = result;
-
-                    return result;
-                }
-
-                currentType = currentType.GetTypeInfo().BaseType;
-            }
-
-            _metadataCache[type] = _defaultMetadata;
-
-            return _defaultMetadata;
+            return GetMetadataWithOverrides(type);
         }
 
         /// <summary>
@@ -535,6 +521,39 @@ namespace Avalonia
             metadata.Merge(baseMetadata, this);
             _metadata.Add(type, metadata);
             _metadataCache.Clear();
+
+            _hasMetadataOverrides = true;
+        }
+
+        private PropertyMetadata GetMetadataWithOverrides(Type type)
+        {
+            if (type is null)
+            {
+                throw new ArgumentNullException(nameof(type));
+            }
+
+            if (_metadataCache.TryGetValue(type, out PropertyMetadata result))
+            {
+                return result;
+            }
+
+            Type currentType = type;
+
+            while (currentType != null)
+            {
+                if (_metadata.TryGetValue(currentType, out result))
+                {
+                    _metadataCache[type] = result;
+
+                    return result;
+                }
+
+                currentType = currentType.GetTypeInfo().BaseType;
+            }
+
+            _metadataCache[type] = _defaultMetadata;
+
+            return _defaultMetadata;
         }
 
         [DebuggerHidden]

+ 23 - 0
src/Avalonia.Base/EnumExtensions.cs

@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Provides extension methods for enums.
+    /// </summary>
+    public static class EnumExtensions
+    {
+        [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;
+        }
+    }
+}

+ 5 - 0
src/Avalonia.Base/Utilities/MathUtilities.cs

@@ -159,6 +159,11 @@ namespace Avalonia.Utilities
         /// <returns>The clamped value.</returns>
         public static int Clamp(int val, int min, int max)
         {
+            if (min > max)
+            {
+                throw new ArgumentException($"{min} cannot be greater than {max}.");
+            }
+
             if (val < min)
             {
                 return min;

+ 6 - 6
src/Avalonia.Base/ValueStore.cs

@@ -57,7 +57,8 @@ namespace Avalonia
                 {
                     if (priority == (int)BindingPriority.LocalValue)
                     {
-                        _propertyValues.SetValue(property, Validate(property, value));
+                        Validate(property, ref value);
+                        _propertyValues.SetValue(property, value);
                         Changed(property, priority, v, value);
                         return;
                     }
@@ -78,7 +79,8 @@ namespace Avalonia
 
                 if (priority == (int)BindingPriority.LocalValue)
                 {
-                    _propertyValues.AddValue(property, Validate(property, value));
+                    Validate(property, ref value);
+                    _propertyValues.AddValue(property, value);
                     Changed(property, priority, AvaloniaProperty.UnsetValue, value);
                     return;
                 }
@@ -166,16 +168,14 @@ namespace Avalonia
                 validate2);
         }
 
-        private object Validate(AvaloniaProperty property, object value)
+        private void Validate(AvaloniaProperty property, ref object value)
         {
             var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
 
             if (validate != null && value != AvaloniaProperty.UnsetValue)
             {
-                return validate(_owner, value);
+                value = validate(_owner, value);
             }
-
-            return value;
         }
 
         private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)

+ 2 - 3
src/Avalonia.Controls/AppBuilderBase.cs

@@ -125,9 +125,8 @@ namespace Avalonia.Controls
             });
             
             // Copy-pasted because we can't call extension methods due to generic constraints
-            var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose};
-            Instance.ApplicationLifetime = lifetime;
-            SetupWithoutStarting();
+            var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = ShutdownMode.OnMainWindowClose};
+            SetupWithLifetime(lifetime);
             lifetime.Start(Array.Empty<string>());
         }
 

+ 8 - 0
src/Avalonia.Controls/Application.cs

@@ -48,6 +48,14 @@ namespace Avalonia
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
+        /// <summary>
+        /// Creates an instance of the <see cref="Application"/> class.
+        /// </summary>
+        public Application()
+        {
+            Name = "Avalonia Application";
+        }
+
         /// <summary>
         /// Gets the current instance of the <see cref="Application"/> class.
         /// </summary>

+ 4 - 5
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -5,12 +5,12 @@ using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
+using Avalonia.Threading;
 
 namespace Avalonia.Controls.ApplicationLifetimes
 {
     public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable
     {
-        private readonly Application _app;
         private int _exitCode;
         private CancellationTokenSource _cts;
         private bool _isShuttingDown;
@@ -34,12 +34,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
             _activeLifetime?._windows.Add((Window)sender);
         }
 
-        public ClassicDesktopStyleApplicationLifetime(Application app)
+        public ClassicDesktopStyleApplicationLifetime()
         {
             if (_activeLifetime != null)
                 throw new InvalidOperationException(
                     "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
-            _app = app;
             _activeLifetime = this;
         }
         
@@ -103,7 +102,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
             Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
             _cts = new CancellationTokenSource();
             MainWindow?.Show();
-            _app.Run(_cts.Token);
+            Dispatcher.UIThread.MainLoop(_cts.Token);
             Environment.ExitCode = _exitCode;
             return _exitCode;
         }
@@ -124,7 +123,7 @@ namespace Avalonia
             this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
             where T : AppBuilderBase<T>, new()
         {
-            var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode};
+            var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode};
             builder.SetupWithLifetime(lifetime);
             return lifetime.Start(args);
         }

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

@@ -704,7 +704,7 @@ namespace Avalonia.Controls
                 added.Add(e.NewValue);
             }
 
-            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed));
         }
 
         /// <summary>

+ 10 - 3
src/Avalonia.Controls/ColumnDefinition.cs

@@ -26,6 +26,16 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<GridLength> WidthProperty =
             AvaloniaProperty.Register<ColumnDefinition, GridLength>(nameof(Width), new GridLength(1, GridUnitType.Star));
 
+        /// <summary>
+        /// Initializes static members of the <see cref="ColumnDefinition"/> class.
+        /// </summary>
+        static ColumnDefinition()
+        {
+            AffectsParentMeasure(MinWidthProperty, MaxWidthProperty);
+
+            WidthProperty.Changed.AddClassHandler<DefinitionBase>(OnUserSizePropertyChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ColumnDefinition"/> class.
         /// </summary>
@@ -68,7 +78,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(MaxWidthProperty, value);
             }
         }
@@ -84,7 +93,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(MinWidthProperty, value);
             }
         }
@@ -100,7 +108,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(WidthProperty, value);
             }
         }

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

@@ -7,9 +7,6 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
-
-using Avalonia;
-using Avalonia.Collections;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls
@@ -50,6 +47,8 @@ namespace Avalonia.Controls
                     }
                 }
             }
+
+            Parent?.InvalidateMeasure();
         }
 
         /// <summary>
@@ -63,6 +62,8 @@ namespace Avalonia.Controls
                 _sharedState.RemoveMember(this);
                 _sharedState = null;
             }
+
+            Parent?.InvalidateMeasure();
         }
 
         /// <summary>
@@ -114,6 +115,36 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <remarks>
+        /// Notifies parent <see cref="Grid"/> or size scope that definition size has been changed.
+        /// </remarks>
+        internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (definition.Parent == null)
+            {
+                return;
+            }
+
+            if (definition._sharedState != null)
+            {
+                definition._sharedState.Invalidate();
+            }
+            else
+            {
+                GridUnitType oldUnitType = ((GridLength)e.OldValue).GridUnitType;
+                GridUnitType newUnitType = ((GridLength)e.NewValue).GridUnitType;
+
+                if (oldUnitType != newUnitType)
+                {
+                    definition.Parent.Invalidate();
+                }
+                else
+                {
+                    definition.Parent.InvalidateMeasure();
+                }
+            }
+        }
+
         /// <summary>
         /// Returns <c>true</c> if this definition is a part of shared group.
         /// </summary>
@@ -730,5 +761,22 @@ namespace Avalonia.Controls
             SharedSizeGroupProperty.Changed.AddClassHandler<DefinitionBase>(OnSharedSizeGroupPropertyChanged);
             PrivateSharedSizeScopeProperty.Changed.AddClassHandler<DefinitionBase>(OnPrivateSharedSizeScopePropertyChanged);
         }
+
+        /// <summary>
+        /// Marks a property on a definition as affecting the parent grid's measurement.
+        /// </summary>
+        /// <param name="properties">The properties.</param>
+        protected static void AffectsParentMeasure(params AvaloniaProperty[] properties)
+        {
+            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            {
+                (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure();
+            }
+
+            foreach (var property in properties)
+            {
+                property.Changed.Subscribe(Invalidate);
+            }
+        }
     }
 }

+ 1 - 0
src/Avalonia.Controls/DesktopApplicationExtensions.cs

@@ -50,6 +50,7 @@ namespace Avalonia.Controls
         /// On desktop-style platforms runs the application's main loop with custom CancellationToken
         /// without setting a lifetime.
         /// </summary>
+        /// <param name="app">The application.</param>
         /// <param name="token">The token to track.</param>
         public static void Run(this Application app, CancellationToken token)
         {

+ 0 - 7
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@@ -19,8 +19,6 @@ namespace Avalonia.Controls.Generators
         {
             var tabItem = (TabItem)base.CreateContainer(item);
 
-            tabItem.ParentTabControl = Owner;
-
             tabItem[~TabControl.TabStripPlacementProperty] = Owner[~TabControl.TabStripPlacementProperty];
 
             if (tabItem.HeaderTemplate == null)
@@ -48,11 +46,6 @@ namespace Avalonia.Controls.Generators
                 tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
             }
 
-            if (tabItem.Content == null)
-            {
-                tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
-            }
-
             return tabItem;
         }
     }

+ 12 - 0
src/Avalonia.Controls/Grid.cs

@@ -6,6 +6,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Diagnostics;
 using System.Linq;
 using System.Threading;
@@ -178,6 +179,7 @@ namespace Avalonia.Controls
                 if (_data == null) { _data = new ExtendedData(); }
                 _data.ColumnDefinitions = value;
                 _data.ColumnDefinitions.Parent = this;
+                InvalidateMeasure();
             }
         }
 
@@ -198,6 +200,7 @@ namespace Avalonia.Controls
                 if (_data == null) { _data = new ExtendedData(); }
                 _data.RowDefinitions = value;
                 _data.RowDefinitions.Parent = this;
+                InvalidateMeasure();
             }
         }
 
@@ -569,6 +572,15 @@ namespace Avalonia.Controls
             return (arrangeSize);
         }
 
+        /// <summary>
+        /// <see cref="Panel.ChildrenChanged"/>
+        /// </summary>
+        protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            CellsStructureDirty = true;
+            base.ChildrenChanged(sender, e);
+        }
+
         /// <summary>
         /// Invalidates grid caches and makes the grid dirty for measure.
         /// </summary>

+ 749 - 118
src/Avalonia.Controls/GridSplitter.cs

@@ -1,210 +1,841 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
+// This source file is adapted from the Windows Presentation Foundation project. 
+// (https://github.com/dotnet/wpf/) 
+// 
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
+using Avalonia.Collections;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
-using Avalonia.VisualTree;
+using Avalonia.Media;
+using Avalonia.Utilities;
 
 namespace Avalonia.Controls
 {
     /// <summary>
-    ///     Represents the control that redistributes space between columns or rows of a Grid control.
+    /// Represents the control that redistributes space between columns or rows of a <see cref="Grid"/> control.
     /// </summary>
-    /// <remarks>
-    ///     Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
-    /// </remarks>
     public class GridSplitter : Thumb
     {
-        private List<DefinitionBase> _definitions;
+        /// <summary>
+        /// Defines the <see cref="ResizeDirection"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<GridResizeDirection> ResizeDirectionProperty =
+            AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
 
-        private Grid _grid;
+        /// <summary>
+        /// Defines the <see cref="ResizeBehavior"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<GridResizeBehavior> ResizeBehaviorProperty =
+            AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
 
-        private DefinitionBase _nextDefinition;
+        /// <summary>
+        /// Defines the <see cref="ShowsPreview"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<bool> ShowsPreviewProperty =
+            AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
 
-        private Orientation _orientation;
+        /// <summary>
+        /// Defines the <see cref="KeyboardIncrement"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> KeyboardIncrementProperty =
+            AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
 
-        private DefinitionBase _prevDefinition;
+        /// <summary>
+        /// Defines the <see cref="DragIncrement"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> DragIncrementProperty =
+            AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
 
-        private void GetDeltaConstraints(out double min, out double max)
+        /// <summary>
+        /// Defines the <see cref="PreviewContent"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<ITemplate<IControl>> PreviewContentProperty =
+            AvaloniaProperty.Register<GridSplitter, ITemplate<IControl>>(nameof(PreviewContent));
+
+        private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast);
+        private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth);
+
+        private ResizeData _resizeData;
+
+        /// <summary>
+        /// Indicates whether the Splitter resizes the Columns, Rows, or Both.
+        /// </summary>
+        public GridResizeDirection ResizeDirection
         {
-            var prevDefinitionLen = GetActualLength(_prevDefinition);
-            var prevDefinitionMin = GetMinLength(_prevDefinition);
-            var prevDefinitionMax = GetMaxLength(_prevDefinition);
+            get => GetValue(ResizeDirectionProperty);
+            set => SetValue(ResizeDirectionProperty, value);
+        }
 
-            var nextDefinitionLen = GetActualLength(_nextDefinition);
-            var nextDefinitionMin = GetMinLength(_nextDefinition);
-            var nextDefinitionMax = GetMaxLength(_nextDefinition);
-            // Determine the minimum and maximum the columns can be resized
-            min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen);
-            max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin);
+        /// <summary>
+        /// Indicates which Columns or Rows the Splitter resizes.
+        /// </summary>
+        public GridResizeBehavior ResizeBehavior
+        {
+            get => GetValue(ResizeBehaviorProperty);
+            set => SetValue(ResizeBehaviorProperty, value);
         }
 
-        protected override void OnDragDelta(VectorEventArgs e)
+        /// <summary>
+        /// Indicates whether to Preview the column resizing without updating layout.
+        /// </summary>
+        public bool ShowsPreview
         {
-            // WPF doesn't change anything when spliter is in the last row/column
-            // but resizes the splitter row/column when it's the first one.
-            // this is different, but more internally consistent.
-            if (_prevDefinition == null || _nextDefinition == null)
-                return;
+            get => GetValue(ShowsPreviewProperty);
+            set => SetValue(ShowsPreviewProperty, value);
+        }
+
+        /// <summary>
+        /// The Distance to move the splitter when pressing the keyboard arrow keys.
+        /// </summary>
+        public double KeyboardIncrement
+        {
+            get => GetValue(KeyboardIncrementProperty);
+            set => SetValue(KeyboardIncrementProperty, value);
+        }
+
+        /// <summary>
+        /// Restricts splitter to move a multiple of the specified units.
+        /// </summary>
+        public double DragIncrement
+        {
+            get => GetValue(DragIncrementProperty);
+            set => SetValue(DragIncrementProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets content that will be shown when <see cref="ShowsPreview"/> is enabled and user starts resize operation.
+        /// </summary>
+        public ITemplate<IControl> PreviewContent
+        {
+            get => GetValue(PreviewContentProperty);
+            set => SetValue(PreviewContentProperty, value);
+        }
+
+        /// <summary>
+        /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height.
+        /// </summary>
+        internal GridResizeDirection GetEffectiveResizeDirection()
+        {
+            GridResizeDirection direction = ResizeDirection;
+
+            if (direction != GridResizeDirection.Auto)
+            {
+                return direction;
+            }
+
+            // When HorizontalAlignment is Left, Right or Center, resize Columns.
+            if (HorizontalAlignment != HorizontalAlignment.Stretch)
+            {
+                direction = GridResizeDirection.Columns;
+            }
+            else if (VerticalAlignment != VerticalAlignment.Stretch)
+            {
+                direction = GridResizeDirection.Rows;
+            }
+            else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height.
+            {
+                direction = GridResizeDirection.Columns;
+            }
+            else
+            {
+                direction = GridResizeDirection.Rows;
+            }
 
-            var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y;
-            double max;
-            double min;
-            GetDeltaConstraints(out min, out max);
-            delta = Math.Min(Math.Max(delta, min), max);
+            return direction;
+        }
 
-            var prevIsStar = IsStar(_prevDefinition);
-            var nextIsStar = IsStar(_nextDefinition);
+        /// <summary>
+        /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
+        /// </summary>
+        private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
+        {
+            GridResizeBehavior resizeBehavior = ResizeBehavior;
 
-            if (prevIsStar && nextIsStar)
+            if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
             {
-                foreach (var definition in _definitions)
+                if (direction == GridResizeDirection.Columns)
                 {
-                    if (definition == _prevDefinition)
+                    switch (HorizontalAlignment)
                     {
-                        SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+                        case HorizontalAlignment.Left:
+                            resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+                            break;
+                        case HorizontalAlignment.Right:
+                            resizeBehavior = GridResizeBehavior.CurrentAndNext;
+                            break;
+                        default:
+                            resizeBehavior = GridResizeBehavior.PreviousAndNext;
+                            break;
                     }
-                    else if (definition == _nextDefinition)
+                }
+                else
+                {
+                    switch (VerticalAlignment)
                     {
-                        SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                        case VerticalAlignment.Top:
+                            resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+                            break;
+                        case VerticalAlignment.Bottom:
+                            resizeBehavior = GridResizeBehavior.CurrentAndNext;
+                            break;
+                        default:
+                            resizeBehavior = GridResizeBehavior.PreviousAndNext;
+                            break;
                     }
-                    else if (IsStar(definition))
+                }
+            }
+
+            return resizeBehavior;
+        }
+
+        /// <summary>
+        /// Removes preview adorner from the grid.
+        /// </summary>
+        private void RemovePreviewAdorner()
+        {
+            if (_resizeData.Adorner != null)
+            {
+                AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
+                layer.Children.Remove(_resizeData.Adorner);
+            }
+        }
+
+        /// <summary>
+        /// Initialize the data needed for resizing.
+        /// </summary>
+        private void InitializeData(bool showsPreview)
+        {
+            // If not in a grid or can't resize, do nothing.
+            if (Parent is Grid grid)
+            {
+                GridResizeDirection resizeDirection = GetEffectiveResizeDirection();
+
+                // Setup data used for resizing.
+                _resizeData = new ResizeData
+                {
+                    Grid = grid,
+                    ShowsPreview = showsPreview,
+                    ResizeDirection = resizeDirection,
+                    SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
+                    ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+                };
+
+                // Store the rows and columns to resize on drag events.
+                if (!SetupDefinitionsToResize())
+                {
+                    // Unable to resize, clear data.
+                    _resizeData = null;
+                    return;
+                }
+
+                // Setup the preview in the adorner if ShowsPreview is true.
+                SetupPreviewAdorner();
+            }
+        }
+
+        /// <summary>
+        /// Returns true if GridSplitter can resize rows/columns.
+        /// </summary>
+        private bool SetupDefinitionsToResize()
+        {
+            int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                Grid.ColumnSpanProperty :
+                Grid.RowSpanProperty);
+
+            if (gridSpan == 1)
+            {
+                var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    Grid.ColumnProperty :
+                    Grid.RowProperty);
+
+                // Select the columns based on behavior.
+                int index1, index2;
+
+                switch (_resizeData.ResizeBehavior)
+                {
+                    case GridResizeBehavior.PreviousAndCurrent:
+                        // Get current and previous.
+                        index1 = splitterIndex - 1;
+                        index2 = splitterIndex;
+                        break;
+                    case GridResizeBehavior.CurrentAndNext:
+                        // Get current and next.
+                        index1 = splitterIndex;
+                        index2 = splitterIndex + 1;
+                        break;
+                    default: // GridResizeBehavior.PreviousAndNext.
+                        // Get previous and next.
+                        index1 = splitterIndex - 1;
+                        index2 = splitterIndex + 1;
+                        break;
+                }
+
+                // Get count of rows/columns in the resize direction.
+                int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    _resizeData.Grid.ColumnDefinitions.Count :
+                    _resizeData.Grid.RowDefinitions.Count;
+
+                if (index1 >= 0 && index2 < count)
+                {
+                    _resizeData.SplitterIndex = splitterIndex;
+
+                    _resizeData.Definition1Index = index1;
+                    _resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection);
+                    _resizeData.OriginalDefinition1Length =
+                        _resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels.
+                    _resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1);
+
+                    _resizeData.Definition2Index = index2;
+                    _resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection);
+                    _resizeData.OriginalDefinition2Length =
+                        _resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels.
+                    _resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2);
+
+                    // Determine how to resize the columns.
+                    bool isStar1 = IsStar(_resizeData.Definition1);
+                    bool isStar2 = IsStar(_resizeData.Definition2);
+
+                    if (isStar1 && isStar2)
                     {
-                        SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
+                        // If they are both stars, resize both.
+                        _resizeData.SplitBehavior = SplitBehavior.Split;
                     }
+                    else
+                    {
+                        // One column is fixed width, resize the first one that is fixed.
+                        _resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2;
+                    }
+
+                    return true;
                 }
             }
-            else if (prevIsStar)
+
+            return false;
+        }
+
+        /// <summary>
+        /// Create the preview adorner and add it to the adorner layer.
+        /// </summary>
+        private void SetupPreviewAdorner()
+        {
+            if (_resizeData.ShowsPreview)
             {
-                SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                // Get the adorner layer and add an adorner to it.
+                var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid);
+
+                var previewContent = PreviewContent;
+
+                // Can't display preview.
+                if (adornerLayer == null)
+                {
+                    return;
+                }
+
+                IControl builtPreviewContent = previewContent?.Build();
+
+                _resizeData.Adorner = new PreviewAdorner(builtPreviewContent);
+
+                AdornerLayer.SetAdornedElement(_resizeData.Adorner, this);
+
+                adornerLayer.Children.Add(_resizeData.Adorner);
+
+                // Get constraints on preview's translation.
+                GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange);
             }
-            else if (nextIsStar)
+        }
+
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            base.OnPointerEnter(e);
+
+            GridResizeDirection direction = GetEffectiveResizeDirection();
+
+            switch (direction)
             {
-                SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+                case GridResizeDirection.Columns:
+                    Cursor = s_columnSplitterCursor;
+                    break;
+                case GridResizeDirection.Rows:
+                    Cursor = s_rowSplitterCursor;
+                    break;
             }
-            else
+        }
+
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+
+            if (_resizeData != null)
             {
-                SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
-                SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                CancelResize();
             }
         }
 
-        private double GetActualLength(DefinitionBase definition)
+        protected override void OnDragStarted(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+            base.OnDragStarted(e);
+
+            // TODO: Looks like that sometimes thumb will raise multiple drag started events.
+            // Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called");
+
+            if (_resizeData != null)
+            {
+                return;
+            }
+
+            InitializeData(ShowsPreview);
         }
 
-        private double GetMinLength(DefinitionBase definition)
+        protected override void OnDragDelta(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
+            base.OnDragDelta(e);
+
+            if (_resizeData != null)
+            {
+                double horizontalChange = e.Vector.X;
+                double verticalChange = e.Vector.Y;
+
+                // Round change to nearest multiple of DragIncrement.
+                double dragIncrement = DragIncrement;
+                horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement;
+                verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement;
+
+                if (_resizeData.ShowsPreview)
+                {
+                    // Set the Translation of the Adorner to the distance from the thumb.
+                    if (_resizeData.ResizeDirection == GridResizeDirection.Columns)
+                    {
+                        _resizeData.Adorner.OffsetX = Math.Min(
+                            Math.Max(horizontalChange, _resizeData.MinChange),
+                            _resizeData.MaxChange);
+                    }
+                    else
+                    {
+                        _resizeData.Adorner.OffsetY = Math.Min(
+                            Math.Max(verticalChange, _resizeData.MinChange),
+                            _resizeData.MaxChange);
+                    }
+                }
+                else
+                {
+                    // Directly update the grid.
+                    MoveSplitter(horizontalChange, verticalChange);
+                }
+            }
         }
 
-        private double GetMaxLength(DefinitionBase definition)
+        protected override void OnDragCompleted(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
+            base.OnDragCompleted(e);
+
+            if (_resizeData != null)
+            {
+                if (_resizeData.ShowsPreview)
+                {
+                    // Update the grid.
+                    MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY);
+                    RemovePreviewAdorner();
+                }
+
+                _resizeData = null;
+            }
         }
 
-        private bool IsStar(DefinitionBase definition)
+        protected override void OnKeyDown(KeyEventArgs e)
         {
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
+            Key key = e.Key;
+
+            switch (key)
+            {
+                case Key.Escape:
+                    if (_resizeData != null)
+                    {
+                        CancelResize();
+                        e.Handled = true;
+                    }
+
+                    break;
+
+                case Key.Left:
+                    e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0);
+                    break;
+                case Key.Right:
+                    e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0);
+                    break;
+                case Key.Up:
+                    e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement);
+                    break;
+                case Key.Down:
+                    e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement);
+                    break;
+            }
         }
 
-        private void SetLengthInStars(DefinitionBase definition, double value)
+        /// <summary>
+        /// Cancels the resize operation.
+        /// </summary>
+        private void CancelResize()
         {
-            var columnDefinition = definition as ColumnDefinition;
-            if (columnDefinition != null)
+            // Restore original column/row lengths.
+            if (_resizeData.ShowsPreview)
             {
-                columnDefinition.Width = new GridLength(value, GridUnitType.Star);
+                RemovePreviewAdorner();
             }
-            else
+            else // Reset the columns/rows lengths to the saved values.
             {
-                ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star);
+                SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length);
+                SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length);
             }
+
+            _resizeData = null;
+        }
+
+        /// <summary>
+        /// Returns true if the row/column has a star length.
+        /// </summary>
+        private static bool IsStar(DefinitionBase definition)
+        {
+            return definition.UserSizeValueCache.IsStar;
         }
 
-        private void SetLength(DefinitionBase definition, double value)
+        /// <summary>
+        /// Gets Column or Row definition at index from grid based on resize direction.
+        /// </summary>
+        private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction)
         {
-            var columnDefinition = definition as ColumnDefinition;
-            if (columnDefinition != null)
+            return direction == GridResizeDirection.Columns ?
+                (DefinitionBase)grid.ColumnDefinitions[index] :
+                (DefinitionBase)grid.RowDefinitions[index];
+        }
+
+        /// <summary>
+        /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row.
+        /// </summary>
+        private double GetActualLength(DefinitionBase definition)
+        {
+            var column = definition as ColumnDefinition;
+
+            return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+        }
+
+        /// <summary>
+        /// Gets Column or Row definition at index from grid based on resize direction.
+        /// </summary>
+        private static void SetDefinitionLength(DefinitionBase definition, GridLength length)
+        {
+            definition.SetValue(
+                definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length);
+        }
+
+        /// <summary>
+        /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth).
+        /// </summary>
+        private void GetDeltaConstraints(out double minDelta, out double maxDelta)
+        {
+            double definition1Len = GetActualLength(_resizeData.Definition1);
+            double definition1Min = _resizeData.Definition1.UserMinSizeValueCache;
+            double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache;
+
+            double definition2Len = GetActualLength(_resizeData.Definition2);
+            double definition2Min = _resizeData.Definition2.UserMinSizeValueCache;
+            double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache;
+
+            // Set MinWidths to be greater than width of splitter.
+            if (_resizeData.SplitterIndex == _resizeData.Definition1Index)
             {
-                columnDefinition.Width = new GridLength(value);
+                definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
             }
-            else
+            else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
             {
-                ((RowDefinition)definition).Height = new GridLength(value);
+                definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength);
             }
+
+            // Determine the minimum and maximum the columns can be resized.
+            minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len);
+            maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min);
         }
 
-        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        /// <summary>
+        /// Sets the length of definition1 and definition2.
+        /// </summary>
+        private void SetLengths(double definition1Pixels, double definition2Pixels)
         {
-            base.OnAttachedToVisualTree(e);
-            _grid = this.GetVisualParent<Grid>();
+            // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values.
+            if (_resizeData.SplitBehavior == SplitBehavior.Split)
+            {
+                var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    (IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.ColumnDefinitions :
+                    (IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.RowDefinitions;
 
-            _orientation = DetectOrientation();
+                var definitionsCount = definitions.Count;
+
+                for (var i = 0; i < definitionsCount; i++)
+                {
+                    DefinitionBase definition = definitions[i];
 
-            int definitionIndex; //row or col
-            if (_orientation == Orientation.Vertical)
+                    // For each definition, if it is a star, set is value to ActualLength in stars
+                    // This makes 1 star == 1 pixel in length
+                    if (i == _resizeData.Definition1Index)
+                    {
+                        SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star));
+                    }
+                    else if (i == _resizeData.Definition2Index)
+                    {
+                        SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star));
+                    }
+                    else if (IsStar(definition))
+                    {
+                        SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star));
+                    }
+                }
+            }
+            else if (_resizeData.SplitBehavior == SplitBehavior.Resize1)
             {
-                Cursor = new Cursor(StandardCursorType.SizeWestEast);
-                _definitions = _grid.ColumnDefinitions.Cast<DefinitionBase>().ToList();
-                definitionIndex = GetValue(Grid.ColumnProperty);
-                PseudoClasses.Add(":vertical");
+                SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels));
             }
             else
             {
-                Cursor = new Cursor(StandardCursorType.SizeNorthSouth);
-                definitionIndex = GetValue(Grid.RowProperty);
-                _definitions = _grid.RowDefinitions.Cast<DefinitionBase>().ToList();
-                PseudoClasses.Add(":horizontal");
+                SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
+            }
+        }
+
+        /// <summary>
+        /// Move the splitter by the given Delta's in the horizontal and vertical directions.
+        /// </summary>
+        private void MoveSplitter(double horizontalChange, double verticalChange)
+        {
+            Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter");
+
+            // Calculate the offset to adjust the splitter.
+            var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
+
+            DefinitionBase definition1 = _resizeData.Definition1;
+            DefinitionBase definition2 = _resizeData.Definition2;
+
+            if (definition1 != null && definition2 != null)
+            {
+                double actualLength1 = GetActualLength(definition1);
+                double actualLength2 = GetActualLength(definition2);
+
+                // When splitting, Check to see if the total pixels spanned by the definitions 
+                // is the same asbefore starting resize. If not cancel the drag
+                if (_resizeData.SplitBehavior == SplitBehavior.Split &&
+                    !MathUtilities.AreClose(
+                        actualLength1 + actualLength2,
+                        _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength))
+                {
+                    CancelResize();
+
+                    return;
+                }
+
+                GetDeltaConstraints(out var min, out var max);
+
+                // Constrain Delta to Min/MaxWidth of columns
+                delta = Math.Min(Math.Max(delta, min), max);
+
+                double definition1LengthNew = actualLength1 + delta;
+                double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew;
+
+                SetLengths(definition1LengthNew, definition2LengthNew);
             }
+        }
 
-            if (definitionIndex > 0)
-                _prevDefinition = _definitions[definitionIndex - 1];
+        /// <summary>
+        /// Move the splitter using the Keyboard (Don't show preview).
+        /// </summary>
+        private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange)
+        {
+            // If moving with the mouse, ignore keyboard motion.
+            if (_resizeData != null)
+            {
+                return false; // Don't handle the event.
+            }
+
+            // Don't show preview.
+            InitializeData(false); 
+                
+            // Check that we are actually able to resize.
+            if (_resizeData == null)
+            {
+                return false; // Don't handle the event.
+            }
 
-            if (definitionIndex < _definitions.Count - 1)
-                _nextDefinition = _definitions[definitionIndex + 1];
+            MoveSplitter(horizontalChange, verticalChange);
+
+            _resizeData = null;
+
+            return true;
         }
 
-        private Orientation DetectOrientation()
+        /// <summary>
+        /// This adorner draws the preview for the <see cref="GridSplitter"/>.
+        /// It also positions the adorner.
+        /// </summary>
+        private sealed class PreviewAdorner : Decorator
         {
-            if (!_grid.ColumnDefinitions.Any())
-                return Orientation.Horizontal;
-            if (!_grid.RowDefinitions.Any())
-                return Orientation.Vertical;
+            private readonly TranslateTransform _translation;
+            private readonly Decorator _decorator;
+            
+            public PreviewAdorner(IControl previewControl)
+            {
+                // Add a decorator to perform translations.
+                _translation = new TranslateTransform();
+
+                _decorator = new Decorator
+                {
+                    Child = previewControl, 
+                    RenderTransform = _translation
+                };
+
+                Child = _decorator;
+            }
 
-            var col = GetValue(Grid.ColumnProperty);
-            var row = GetValue(Grid.RowProperty);
-            var width = _grid.ColumnDefinitions[col].Width;
-            var height = _grid.RowDefinitions[row].Height;
-            if (width.IsAuto && !height.IsAuto)
+            /// <summary>
+            /// The Preview's Offset in the X direction from the GridSplitter.
+            /// </summary>
+            public double OffsetX
             {
-                return Orientation.Vertical;
+                get => _translation.X;
+                set => _translation.X = value;
             }
-            if (!width.IsAuto && height.IsAuto)
+
+            /// <summary>
+            /// The Preview's Offset in the Y direction from the GridSplitter.
+            /// </summary>
+            public double OffsetY
             {
-                return Orientation.Horizontal;
+                get => _translation.Y;
+                set => _translation.Y = value;
             }
-            if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column
-                .Where(c => Grid.GetColumn(c) == col)
-                .Any(c => c.GetType() != typeof(GridSplitter)))
+
+            protected override Size ArrangeOverride(Size finalSize)
             {
-                return Orientation.Horizontal;
+                // Adorners always get clipped to the owner control. In this case we want
+                // to constrain size to the splitter size but draw on top of the parent grid.
+                Clip = null;
+
+                return base.ArrangeOverride(finalSize);
             }
-            return Orientation.Vertical;
         }
+
+        /// <summary>
+        /// <see cref="GridSplitter"/> has special Behavior when columns are fixed.
+        /// If the left column is fixed, splitter will only resize that column.
+        /// Else if the right column is fixed, splitter will only resize the right column.
+        /// </summary>
+        private enum SplitBehavior
+        {
+            /// <summary>
+            /// Both columns/rows are star lengths.
+            /// </summary>
+            Split,
+
+            /// <summary>
+            /// Resize 1 only.
+            /// </summary>
+            Resize1,
+
+            /// <summary>
+            /// Resize 2 only.
+            /// </summary>
+            Resize2
+        }
+
+        /// <summary>
+        /// Stores data during the resizing operation.
+        /// </summary>
+        private class ResizeData
+        {
+            public bool ShowsPreview;
+            public PreviewAdorner Adorner;
+
+            // The constraints to keep the Preview within valid ranges.
+            public double MinChange;
+            public double MaxChange;
+
+            // The grid to Resize.
+            public Grid Grid;
+
+            // Cache of Resize Direction and Behavior.
+            public GridResizeDirection ResizeDirection;
+            public GridResizeBehavior ResizeBehavior;
+
+            // The columns/rows to resize.
+            public DefinitionBase Definition1;
+            public DefinitionBase Definition2;
+
+            // Are the columns/rows star lengths.
+            public SplitBehavior SplitBehavior;
+
+            // The index of the splitter.
+            public int SplitterIndex;
+
+            // The indices of the columns/rows.
+            public int Definition1Index;
+            public int Definition2Index;
+
+            // The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize).
+            public GridLength OriginalDefinition1Length;
+            public GridLength OriginalDefinition2Length;
+            public double OriginalDefinition1ActualLength;
+            public double OriginalDefinition2ActualLength;
+
+            // The minimum of Width/Height of Splitter.  Used to ensure splitter 
+            // isn't hidden by resizing a row/column smaller than the splitter.
+            public double SplitterLength;
+        }
+    }
+
+    /// <summary>
+    /// Enum to indicate whether <see cref="GridSplitter"/> resizes Columns or Rows.
+    /// </summary>
+    public enum GridResizeDirection
+    {
+        /// <summary>
+        /// Determines whether to resize rows or columns based on its Alignment and 
+        /// width compared to height.
+        /// </summary>
+        Auto,
+
+        /// <summary>
+        /// Resize columns when dragging Splitter.
+        /// </summary>
+        Columns,
+
+        /// <summary>
+        /// Resize rows when dragging Splitter.
+        /// </summary>
+        Rows
+    }
+
+    /// <summary>
+    /// Enum to indicate what Columns or Rows the <see cref="GridSplitter"/> resizes.
+    /// </summary>
+    public enum GridResizeBehavior
+    {
+        /// <summary>
+        /// Determine which columns or rows to resize based on its Alignment.
+        /// </summary>
+        BasedOnAlignment,
+
+        /// <summary>
+        /// Resize the current and next Columns or Rows.
+        /// </summary>
+        CurrentAndNext,
+
+        /// <summary>
+        /// Resize the previous and current Columns or Rows.
+        /// </summary>
+        PreviousAndCurrent,
+
+        /// <summary>
+        /// Resize the previous and next Columns or Rows.
+        /// </summary>
+        PreviousAndNext
     }
 }

+ 9 - 1
src/Avalonia.Controls/ItemsControl.cs

@@ -359,6 +359,12 @@ namespace Avalonia.Controls
             UpdateItemCount();
             RemoveControlItemsFromLogicalChildren(oldValue);
             AddControlItemsToLogicalChildren(newValue);
+
+            if (Presenter != null)
+            {
+                Presenter.Items = newValue;
+            }
+
             SubscribeToItems(newValue);
         }
 
@@ -370,6 +376,8 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
+            UpdateItemCount();
+
             switch (e.Action)
             {
                 case NotifyCollectionChangedAction.Add:
@@ -381,7 +389,7 @@ namespace Avalonia.Controls
                     break;
             }
 
-            UpdateItemCount();
+            Presenter?.ItemsChanged(e);
 
             var collection = sender as ICollection;
             PseudoClasses.Set(":empty", collection == null || collection.Count == 0);

+ 14 - 8
src/Avalonia.Controls/ListBox.cs

@@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -132,21 +133,26 @@ namespace Avalonia.Controls
         {
             base.OnPointerPressed(e);
 
-            if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
+            if (e.Source is IVisual source)
             {
-                e.Handled = UpdateSelectionFromEventSource(
-                    e.Source,
-                    true,
-                    (e.InputModifiers & InputModifiers.Shift) != 0,
-                    (e.InputModifiers & InputModifiers.Control) != 0,
-                    e.MouseButton == MouseButton.Right);
+                var point = e.GetCurrentPoint(source);
+
+                if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
+                {
+                    e.Handled = UpdateSelectionFromEventSource(
+                        e.Source,
+                        true,
+                        (e.KeyModifiers & KeyModifiers.Shift) != 0,
+                        (e.KeyModifiers & KeyModifiers.Control) != 0,
+                        point.Properties.IsRightButtonPressed);
+                }
             }
         }
 
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
-            base.OnTemplateApplied(e);
             Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
+            base.OnTemplateApplied(e);
         }
     }
 }

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

@@ -804,9 +804,9 @@ namespace Avalonia.Controls
 
         private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
         {
-            if (e.Device.Captured != Spinner)
+            if (e.Pointer.Captured != Spinner)
             {
-                Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
+                Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input);
             }
         }
 

+ 7 - 0
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@@ -1,12 +1,19 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System.Collections;
+using System.Collections.Specialized;
+
 namespace Avalonia.Controls.Presenters
 {
     public interface IItemsPresenter : IPresenter
     {
+        IEnumerable Items { get; set; }
+
         IPanel Panel { get; }
 
+        void ItemsChanged(NotifyCollectionChangedEventArgs e);
+
         void ScrollIntoView(object item);
     }
 }

+ 13 - 2
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters
                 _itemsSubscription?.Dispose();
                 _itemsSubscription = null;
 
-                if (_createdPanel && value is INotifyCollectionChanged incc)
+                if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
                 {
                     _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
                 }
@@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters
             private set;
         }
 
+        protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
+
         /// <inheritdoc/>
         public override sealed void ApplyTemplate()
         {
@@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters
         {
         }
 
+        /// <inheritdoc/>
+        void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (Panel != null)
+            {
+                ItemsChanged(e);
+            }
+        }
+
         /// <summary>
         /// Creates the <see cref="ItemContainerGenerator"/> for the control.
         /// </summary>
@@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters
 
             _createdPanel = true;
 
-            if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
+            if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
             {
                 _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
             }

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

@@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters
                 return new FormattedText
                 {
                     Text = "X",
-                    Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                    Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+                    FontSize = FontSize,
                     TextAlignment = TextAlignment,
                     Constraint = availableSize,
                 }.Bounds.Size;

+ 2 - 0
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -41,6 +41,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Initializes a new instance of the <see cref="PopupRoot"/> class.
         /// </summary>
+        /// <param name="parent">The popup parent.</param>
+        /// <param name="impl">The popup implementation.</param>
         /// <param name="dependencyResolver">
         /// The dependency resolver to use. If null the default dependency resolver will be used.
         /// </param>

+ 27 - 12
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
-            base.ItemsCollectionChanged(sender, e);
-
             if (_updateCount > 0)
             {
+                base.ItemsCollectionChanged(sender, e);
                 return;
             }
 
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+                    _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+                    _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
+                    break;
+            }
+
+            base.ItemsCollectionChanged(sender, e);
+
             switch (e.Action)
             {
                 case NotifyCollectionChangedAction.Add:
@@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives
                     }
                     else
                     {
-                        _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
                         UpdateSelectedItem(_selection.First(), false);
                     }
 
                     break;
 
                 case NotifyCollectionChangedAction.Remove:
-                    _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
                     UpdateSelectedItem(_selection.First(), false);
                     ResetSelectedItems();
                     break;
@@ -358,17 +367,17 @@ namespace Avalonia.Controls.Primitives
             {
                 if ((container.ContainerControl as ISelectable)?.IsSelected == true)
                 {
-                    if (SelectedIndex == -1)
-                    {
-                        SelectedIndex = container.Index;
-                    }
-                    else
+                    if (SelectionMode.HasFlag(SelectionMode.Multiple))
                     {
                         if (_selection.Add(container.Index))
                         {
                             resetSelectedItems = true;
                         }
                     }
+                    else
+                    {
+                        SelectedIndex = container.Index;
+                    }
 
                     MarkContainerSelected(container.ContainerControl, true);
                 }
@@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives
                 }
                 else
                 {
-                    SelectedIndex = _updateSelectedIndex != int.MinValue ?
-                        _updateSelectedIndex :
-                        AlwaysSelected ? 0 : -1;
+                    if (_updateSelectedIndex != int.MinValue)
+                    {
+                        SelectedIndex = _updateSelectedIndex;
+                    }
+
+                    if (AlwaysSelected && SelectedIndex == -1)
+                    {
+                        SelectedIndex = 0;
+                    }
                 }
             }
         }

+ 8 - 2
src/Avalonia.Controls/Primitives/TabStrip.cs

@@ -5,6 +5,7 @@ using Avalonia.Controls.Generators;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Layout;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -44,9 +45,14 @@ namespace Avalonia.Controls.Primitives
         {
             base.OnPointerPressed(e);
 
-            if (e.MouseButton == MouseButton.Left)
+            if (e.Source is IVisual source)
             {
-                e.Handled = UpdateSelectionFromEventSource(e.Source);
+                var point = e.GetCurrentPoint(source);
+
+                if (point.Properties.IsLeftButtonPressed)
+                {
+                    e.Handled = UpdateSelectionFromEventSource(e.Source);
+                }
             }
         }
     }

+ 0 - 2
src/Avalonia.Controls/Primitives/Thumb.cs

@@ -73,7 +73,6 @@ namespace Avalonia.Controls.Primitives
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
-            e.Device.Capture(this);
             e.Handled = true;
             _lastPoint = e.GetPosition(this);
 
@@ -92,7 +91,6 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastPoint.HasValue)
             {
-                e.Device.Capture(null);
                 e.Handled = true;
                 _lastPoint = null;
 

+ 11 - 1
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -565,7 +565,17 @@ namespace Avalonia.Controls
                 if (Layout is VirtualizingLayout virtualLayout)
                 {
                     var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
-                    virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
+
+                    _processingItemsSourceChange = args;
+
+                    try
+                    {
+                        virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
+                    }
+                    finally
+                    {
+                        _processingItemsSourceChange = null;
+                    }
                 }
                 else if (Layout is NonVirtualizingLayout nonVirtualLayout)
                 {

+ 10 - 3
src/Avalonia.Controls/RowDefinition.cs

@@ -26,6 +26,16 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<GridLength> HeightProperty =
             AvaloniaProperty.Register<RowDefinition, GridLength>(nameof(Height), new GridLength(1, GridUnitType.Star));
 
+        /// <summary>
+        /// Initializes static members of the <see cref="RowDefinition"/> class.
+        /// </summary>
+        static RowDefinition()
+        {
+            AffectsParentMeasure(MaxHeightProperty, MinHeightProperty);
+
+            HeightProperty.Changed.AddClassHandler<DefinitionBase>(OnUserSizePropertyChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RowDefinition"/> class.
         /// </summary>
@@ -68,7 +78,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(MaxHeightProperty, value);
             }
         }
@@ -84,7 +93,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(MinHeightProperty, value);
             }
         }
@@ -100,7 +108,6 @@ namespace Avalonia.Controls
             }
             set
             {
-                Parent?.InvalidateMeasure();
                 SetValue(HeightProperty, value);
             }
         }

+ 3 - 12
src/Avalonia.Controls/Shapes/Shape.cs

@@ -2,7 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Reflection;
 using Avalonia.Collections;
 using Avalonia.Media;
 
@@ -166,12 +165,9 @@ namespace Avalonia.Controls.Shapes
             {
                 property.Changed.Subscribe(e =>
                 {
-                    var senderType = e.Sender.GetType().GetTypeInfo();
-                    var affectedType = typeof(TShape).GetTypeInfo();
-
-                    if (affectedType.IsAssignableFrom(senderType))
+                    if (e.Sender is TShape shape)
                     {
-                        AffectsGeometryInvalidate(e);
+                        AffectsGeometryInvalidate(shape, e);
                     }
                 });
             }
@@ -322,13 +318,8 @@ namespace Avalonia.Controls.Shapes
             return (size, transform);
         }
 
-        private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
+        private static void AffectsGeometryInvalidate(Shape control, AvaloniaPropertyChangedEventArgs e)
         {
-            if (!(e.Sender is Shape control))
-            {
-                return;
-            }
-
             // If the geometry is invalidated when Bounds changes, only invalidate when the Size
             // portion changes.
             if (e.Property == BoundsProperty)

+ 56 - 1
src/Avalonia.Controls/TabControl.cs

@@ -4,7 +4,6 @@
 using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
-using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -70,6 +69,7 @@ namespace Avalonia.Controls
             SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
             ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
             AffectsMeasure<TabControl>(TabStripPlacementProperty);
+            SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e));
         }
 
         /// <summary>
@@ -145,6 +145,61 @@ namespace Avalonia.Controls
             return RegisterContentPresenter(presenter);
         }
 
+        protected override void OnContainersMaterialized(ItemContainerEventArgs e)
+        {
+            base.OnContainersMaterialized(e);
+
+            if (SelectedContent != null || SelectedIndex == -1)
+            {
+                return;
+            }
+
+            var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex);
+
+            if (container == null)
+            {
+                return;
+            }
+
+            UpdateSelectedContent(container);
+        }
+
+        private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
+        {
+            var index = (int)e.NewValue;
+
+            if (index == -1)
+            {
+                SelectedContentTemplate = null;
+
+                SelectedContent = null;
+
+                return;
+            }
+
+            var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index);
+
+            if (container == null)
+            {
+                return;
+            }
+
+            UpdateSelectedContent(container);
+        }
+
+        private void UpdateSelectedContent(IContentControl item)
+        {
+            if (SelectedContentTemplate != item.ContentTemplate)
+            {
+                SelectedContentTemplate = item.ContentTemplate;
+            }
+
+            if (SelectedContent != item.Content)
+            {
+                SelectedContent = item.Content;
+            }
+        }
+
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>

+ 0 - 21
src/Avalonia.Controls/TabItem.cs

@@ -30,7 +30,6 @@ namespace Avalonia.Controls
         {
             SelectableMixin.Attach<TabItem>(IsSelectedProperty);
             FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
-            IsSelectedProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateSelectedContent(e));
             DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
         }
 
@@ -54,8 +53,6 @@ namespace Avalonia.Controls
             set { SetValue(IsSelectedProperty, value); }
         }
 
-        internal TabControl ParentTabControl { get; set; }
-
         private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
         {
             if (Header == null)
@@ -83,23 +80,5 @@ namespace Avalonia.Controls
                 }
             }          
         }
-
-        private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e)
-        {
-            if (!IsSelected)
-            {
-                return;
-            }
-
-            if (ParentTabControl.SelectedContentTemplate != ContentTemplate)
-            {
-                ParentTabControl.SelectedContentTemplate = ContentTemplate;
-            }
-
-            if (ParentTabControl.SelectedContent != Content)
-            {
-                ParentTabControl.SelectedContent = Content;
-            }
-        }
     }
 }

+ 3 - 2
src/Avalonia.Controls/TextBlock.cs

@@ -352,10 +352,11 @@ namespace Avalonia.Controls
             return new FormattedText
             {
                 Constraint = constraint,
-                Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+                FontSize = FontSize,
                 Text = text ?? string.Empty,
                 TextAlignment = TextAlignment,
-                Wrapping = TextWrapping,
+                TextWrapping = TextWrapping,
             };
         }
 

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

@@ -677,13 +677,13 @@ namespace Avalonia.Controls
                 }
             }
 
-            e.Device.Capture(_presenter);
+            e.Pointer.Capture(_presenter);
             e.Handled = true;
         }
 
         protected override void OnPointerMoved(PointerEventArgs e)
         {
-            if (_presenter != null && e.Device.Captured == _presenter)
+            if (_presenter != null && e.Pointer.Captured == _presenter)
             {
                 var point = e.GetPosition(_presenter);
 
@@ -694,9 +694,9 @@ namespace Avalonia.Controls
 
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         {
-            if (_presenter != null && e.Device.Captured == _presenter)
+            if (_presenter != null && e.Pointer.Captured == _presenter)
             {
-                e.Device.Capture(null);
+                e.Pointer.Capture(null);
             }
         }
 

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

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.Input;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -79,7 +80,10 @@ namespace Avalonia.Controls
         {
             StopTimer();
 
-            ToolTip.SetIsOpen(control, true);
+            if ((control as IVisual).IsAttachedToVisualTree)
+            {
+                ToolTip.SetIsOpen(control, true);
+            }
         }
 
         private void Close(Control control)

+ 12 - 7
src/Avalonia.Controls/TreeView.cs

@@ -507,14 +507,19 @@ namespace Avalonia.Controls
         {
             base.OnPointerPressed(e);
 
-            if (e.MouseButton == MouseButton.Left || e.MouseButton == MouseButton.Right)
+            if (e.Source is IVisual source)
             {
-                e.Handled = UpdateSelectionFromEventSource(
-                    e.Source,
-                    true,
-                    (e.InputModifiers & InputModifiers.Shift) != 0,
-                    (e.InputModifiers & InputModifiers.Control) != 0,
-                    e.MouseButton == MouseButton.Right);
+                var point = e.GetCurrentPoint(source);
+
+                if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
+                {
+                    e.Handled = UpdateSelectionFromEventSource(
+                        e.Source,
+                        true,
+                        (e.KeyModifiers & KeyModifiers.Shift) != 0,
+                        (e.KeyModifiers & KeyModifiers.Control) != 0,
+                        point.Properties.IsRightButtonPressed);
+                }
             }
         }
 

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

@@ -42,7 +42,7 @@ namespace Avalonia.Controls
         /// </summary>
         static WrapPanel()
         {
-            AffectsMeasure<WrapPanel>(OrientationProperty);
+            AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@@ -2,7 +2,7 @@
              xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="Avalonia.Diagnostics.Views.TreePageView">
-  <Grid ColumnDefinitions="*,4,3*">
+  <Grid ColumnDefinitions="*,Auto,3*">
     <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
       <TreeView.DataTemplates>
         <TreeDataTemplate DataType="vm:TreeNode"
@@ -20,7 +20,7 @@
       </TreeView.Styles>
     </TreeView>
 
-    <GridSplitter Width="4" Grid.Column="1" />
+    <GridSplitter Grid.Column="1" />
     <ContentControl Content="{Binding Details}" Grid.Column="2" />
   </Grid>
 </UserControl>

+ 105 - 0
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

@@ -0,0 +1,105 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        MaxWidth="400"
+        MaxHeight="475"
+        MinWidth="430"
+        MinHeight="475"
+        Title="About Avalonia"
+        Background="Purple"
+        FontFamily="/Assets/Roboto-Light.ttf#Roboto"
+        x:Class="Avalonia.Dialogs.AboutAvaloniaDialog">
+  <Window.Styles>
+    <Style>
+      <Style.Resources>
+        <DrawingGroup x:Key="AvaloniaLogo">
+          <GeometryDrawing Geometry="m 150.66581 0.66454769 c -54.77764 0 -101.0652 38.86360031 -112.62109 90.33008031 a 26.1 26.1 0 0 1 18.92187 25.070312 26.1 26.1 0 0 1 -18.91992 25.08202 c 11.56024 51.46073 57.8456 90.31837 112.61914 90.31837 63.37832 0 115.40039 -52.02207 115.40039 -115.40039 0 -63.378322 -52.02207 -115.40039231 -115.40039 -115.40039231 z m 0 60.00000031 c 30.95192 0 55.40039 24.44847 55.40039 55.400392 0 30.9519 -24.44847 55.40039 -55.40039 55.40039 -30.95191 0 -55.40039 -24.44848 -55.40039 -55.40039 0 -30.951922 24.44848 -55.400392 55.40039 -55.400392 z">
+            <GeometryDrawing.Brush>
+              <LinearGradientBrush StartPoint="272,411" EndPoint="435,248">
+                <LinearGradientBrush.GradientStops>
+                  <GradientStop Color="#B0B0B0" Offset="0" />
+                  <GradientStop Color="#FFFFFF" Offset="0.6784" />
+                </LinearGradientBrush.GradientStops>
+              </LinearGradientBrush>
+            </GeometryDrawing.Brush>
+          </GeometryDrawing>
+          <GeometryDrawing Brush="#B0B0B0">
+            <GeometryDrawing.Geometry>
+              <EllipseGeometry Rect="9.6,95.8,40.6,40.6" />
+            </GeometryDrawing.Geometry>
+          </GeometryDrawing>
+          <GeometryDrawing Brush="White">
+            <GeometryDrawing.Geometry>
+              <RectangleGeometry Rect="206.06355, 114.56503,60,116.2" />
+            </GeometryDrawing.Geometry>
+          </GeometryDrawing>
+        </DrawingGroup>
+      </Style.Resources>
+    </Style>
+    <Style Selector="Rectangle.Abstract">
+      <Setter Property="Fill" Value="White" />
+      <Setter Property="Width" Value="750" />
+      <Setter Property="Height" Value="700" />
+    </Style>
+    <Style Selector="Button.Hyperlink">
+      <Setter Property="Background" Value="Transparent" />
+      <Setter Property="BorderThickness" Value="0" />
+      <Setter Property="Margin" Value="-5"/>
+      <Setter Property="Foreground" Value="#419df2" />
+      <Setter Property="Command" Value="{Binding OpenBrowser}" />
+      <Setter Property="Content" Value="{Binding $self.CommandParameter}" />
+      <Setter Property="HorizontalAlignment" Value="Center" />
+      <Setter Property="Cursor" Value="Hand" />
+    </Style>
+  </Window.Styles>
+  <Grid Background="#4A255D">
+    <Canvas>
+      <Rectangle Classes="Abstract" Canvas.Top="90" Opacity="0.132">
+        <Rectangle.RenderTransform>
+          <RotateTransform Angle="-2" />
+        </Rectangle.RenderTransform>
+      </Rectangle>
+      <Rectangle Classes="Abstract" Canvas.Top="95" Opacity="0.3">
+        <Rectangle.RenderTransform>
+          <RotateTransform Angle="-4" />
+        </Rectangle.RenderTransform>
+      </Rectangle>
+      <Rectangle Classes="Abstract" Canvas.Top="100" Opacity="0.3">
+        <Rectangle.RenderTransform>
+          <RotateTransform Angle="-8" />
+        </Rectangle.RenderTransform>
+      </Rectangle>
+      <Rectangle Classes="Abstract" Canvas.Top="105" Opacity="0.7">
+        <Rectangle.RenderTransform>
+          <RotateTransform Angle="-12" />
+        </Rectangle.RenderTransform>
+      </Rectangle>
+    </Canvas>
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="18">
+      <Border Height="70" Width="70">
+        <DrawingPresenter Drawing="{DynamicResource AvaloniaLogo}" />
+      </Border>
+      <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10,-10,0,0">
+        <TextBlock Text="Avalonia 0.9" FontSize="40" Foreground="White" />
+        <TextBlock Text="Development Build" Margin="0,-10,0,0" FontSize="15" Foreground="White" />
+      </StackPanel>
+    </StackPanel>
+    <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Spacing="20" Margin="10 60 10 0">
+      <TextBlock Text="This product is built with the Avalonia cross-platform UI Framework. &#x0A;&#x0A;Avalonia is made possible by the generous support of it's contributors and community." TextWrapping="Wrap" TextAlignment="Center" HorizontalAlignment="Center" />
+      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
+        <TextBlock Text="Main source repository | " />
+        <Button Classes="Hyperlink" CommandParameter="https://github.com/AvaloniaUI/Avalonia/"  />
+      </StackPanel>
+      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
+        <TextBlock Text="Documentation and Information | " />
+        <Button Classes="Hyperlink" CommandParameter="https://avaloniaui.net/"  />
+      </StackPanel>
+      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
+        <TextBlock Text="Chat Room | " />
+        <Button Classes="Hyperlink" CommandParameter="https://gitter.im/AvaloniaUI/Avalonia/"  />
+      </StackPanel>
+     </StackPanel> 
+    <StackPanel VerticalAlignment="Bottom" Margin="10">
+      <TextBlock Text="© 2019 The Avalonia Project" TextWrapping="Wrap" HorizontalAlignment="Center" />
+    </StackPanel>
+  </Grid>
+</Window>

+ 62 - 0
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@@ -0,0 +1,62 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Avalonia.Dialogs
+{
+    public class AboutAvaloniaDialog : Window
+    {
+        public AboutAvaloniaDialog()
+        {
+            AvaloniaXamlLoader.Load(this);
+            DataContext = this;
+        }
+ 
+        public static void OpenBrowser(string url)
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                // If no associated application/json MimeType is found xdg-open opens retrun error
+                // but it tries to open it anyway using the console editor (nano, vim, other..)
+                ShellExec($"xdg-open {url}", waitForExit: false);
+            }
+            else
+            {
+                using (Process process = Process.Start(new ProcessStartInfo
+                {
+                    FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
+                    Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"-e {url}" : "",
+                    CreateNoWindow = true,
+                    UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+                }));
+            }
+        }
+
+        private static void ShellExec(string cmd, bool waitForExit = true)
+        {
+            var escapedArgs = cmd.Replace("\"", "\\\"");
+
+            using (var process = Process.Start(
+                new ProcessStartInfo
+                {
+                    FileName = "/bin/sh",
+                    Arguments = $"-c \"{escapedArgs}\"",
+                    RedirectStandardOutput = true,
+                    UseShellExecute = false,
+                    CreateNoWindow = true,
+                    WindowStyle = ProcessWindowStyle.Hidden
+                }
+            ))
+            {
+                if (waitForExit)
+                {
+                    process.WaitForExit();
+                }
+            }
+        }
+    }
+}

BIN
src/Avalonia.Dialogs/Assets/Roboto-Light.ttf


+ 1 - 0
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@@ -7,6 +7,7 @@
     <AvaloniaResource Include="**\*.xaml">
       <SubType>Designer</SubType>
     </AvaloniaResource>
+    <AvaloniaResource Include="Assets\*" />
   </ItemGroup>
 
   <Import Project="..\..\build\BuildTargets.targets" />

+ 6 - 1
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@@ -210,7 +210,12 @@ namespace Avalonia.Dialogs
 
                         if (!_selectingDirectory)
                         {
-                            FileName = SelectedItems.FirstOrDefault()?.DisplayName;
+                            var selectedItem = SelectedItems.FirstOrDefault();
+                            
+						    if (selectedItem != null)
+						    {
+						        FileName = selectedItem.DisplayName;
+						    }
                         }
                     }
                 }

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

@@ -182,7 +182,7 @@ namespace Avalonia.Input
         {
             bool menuIsOpen = MainMenu?.IsOpen == true;
 
-            if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen)
+            if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || 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.

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

@@ -180,7 +180,7 @@ namespace Avalonia.Input
 
             if (sender == e.Source && ev.MouseButton == MouseButton.Left)
             {
-                var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement);
+                var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement);
 
                 if (element == null || !CanFocus(element))
                 {

+ 11 - 2
src/Avalonia.Input/MouseDevice.cs

@@ -14,13 +14,14 @@ namespace Avalonia.Input
     /// <summary>
     /// Represents a mouse device.
     /// </summary>
-    public class MouseDevice : IMouseDevice
+    public class MouseDevice : IMouseDevice, IDisposable
     {
         private int _clickCount;
         private Rect _lastClickRect;
         private ulong _lastClickTime;
 
         private readonly Pointer _pointer;
+        private bool _disposed;
 
         public MouseDevice(Pointer pointer = null)
         {
@@ -126,7 +127,9 @@ namespace Avalonia.Input
         {
             Contract.Requires<ArgumentNullException>(e != null);
 
-            var mouse = (IMouseDevice)e.Device;
+            var mouse = (MouseDevice)e.Device;
+            if(mouse._disposed)
+                return;
 
             Position = e.Root.PointToScreen(e.Position);
             var props = CreateProperties(e);
@@ -441,5 +444,11 @@ namespace Avalonia.Input
                 el = (IInputElement)el.VisualParent;
             }
         }
+
+        public void Dispose()
+        {
+            _disposed = true;
+            _pointer?.Dispose();
+        }
     }
 }

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

@@ -37,7 +37,7 @@ namespace Avalonia.Input
         {
             if (Captured != null)
                 Captured.DetachedFromVisualTree -= OnCaptureDetached;
-            var oldCapture = control;
+            var oldCapture = Captured;
             Captured = control;
             PlatformCapture(control);
             if (oldCapture != null)

+ 6 - 3
src/Avalonia.Input/PointerPoint.cs

@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 namespace Avalonia.Input
 {
     public sealed class PointerPoint
@@ -27,9 +30,9 @@ namespace Avalonia.Input
         public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
         {
             PointerUpdateKind = kind;
-            IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton);
-            IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton);
-            IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton);
+            IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
+            IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
+            IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
 
             // The underlying input source might be reporting the previous state,
             // so make sure that we reflect the current state

+ 18 - 3
src/Avalonia.Input/TouchDevice.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Input.Raw;
@@ -11,10 +12,11 @@ namespace Avalonia.Input
     /// This class is supposed to be used on per-toplevel basis, don't use a shared one
     /// </remarks>
     /// </summary>
-    public class TouchDevice : IInputDevice
+    public class TouchDevice : IInputDevice, IDisposable
     {
-        Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
-
+        private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
+        private bool _disposed;
+        
         KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
             (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
 
@@ -28,6 +30,8 @@ namespace Avalonia.Input
         
         public void ProcessRawEvent(RawInputEventArgs ev)
         {
+            if(_disposed)
+                return;
             var args = (RawTouchEventArgs)ev;
             if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
             {
@@ -82,6 +86,17 @@ namespace Avalonia.Input
 
             
         }
+
+        public void Dispose()
+        {
+            if(_disposed)
+                return;
+            var values = _pointers.Values.ToList();
+            _pointers.Clear();
+            _disposed = true;
+            foreach (var p in values)
+                p.Dispose();
+        }
         
     }
 }

+ 5 - 2
src/Avalonia.Layout/LayoutHelper.cs

@@ -21,8 +21,11 @@ namespace Avalonia.Layout
         /// <returns>The control's size.</returns>
         public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
         {
-            double width = (control.Width > 0) ? control.Width : constraints.Width;
-            double height = (control.Height > 0) ? control.Height : constraints.Height;
+            var controlWidth = control.Width;
+            var controlHeight = control.Height;
+
+            double width = (controlWidth > 0) ? controlWidth : constraints.Width;
+            double height = (controlHeight > 0) ? controlHeight : constraints.Height;
             width = Math.Min(width, control.MaxWidth);
             width = Math.Max(width, control.MinWidth);
             height = Math.Min(height, control.MaxHeight);

+ 7 - 1
src/Avalonia.Layout/LayoutManager.cs

@@ -15,9 +15,15 @@ namespace Avalonia.Layout
     {
         private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
         private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
+        private readonly Action _executeLayoutPass;
         private bool _queued;
         private bool _running;
 
+        public LayoutManager()
+        {
+            _executeLayoutPass = ExecuteLayoutPass;
+        }
+
         /// <inheritdoc/>
         public void InvalidateMeasure(ILayoutable control)
         {
@@ -215,7 +221,7 @@ namespace Avalonia.Layout
         {
             if (!_queued && !_running)
             {
-                Dispatcher.UIThread.Post(ExecuteLayoutPass, DispatcherPriority.Layout);
+                Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
                 _queued = true;
             }
         }

+ 17 - 8
src/Avalonia.Layout/LayoutQueue.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
 
 namespace Avalonia.Layout
 {
@@ -18,9 +17,11 @@ namespace Avalonia.Layout
             _shouldEnqueue = shouldEnqueue;
         }
 
-        private Func<T, bool> _shouldEnqueue;
-        private Queue<T> _inner = new Queue<T>();
-        private Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>();
+        private readonly Func<T, bool> _shouldEnqueue;
+        private readonly Queue<T> _inner = new Queue<T>();
+        private readonly Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>();
+        private readonly List<KeyValuePair<T, Info>> _notFinalizedBuffer = new List<KeyValuePair<T, Info>>();
+
         private int _maxEnqueueCountPerLoop = 1;
 
         public int Count => _inner.Count;
@@ -60,13 +61,19 @@ namespace Avalonia.Layout
 
         public void EndLoop()
         {
-            var notfinalized = _loopQueueInfo.Where(v => v.Value.Count >= _maxEnqueueCountPerLoop).ToArray();
+            foreach (KeyValuePair<T, Info> info in _loopQueueInfo)
+            {
+                if (info.Value.Count >= _maxEnqueueCountPerLoop)
+                {
+                    _notFinalizedBuffer.Add(info);
+                }
+            }
 
             _loopQueueInfo.Clear();
 
-            //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
-            //one more time as a final attempt
-            foreach (var item in notfinalized)
+            // Prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
+            // one more time as a final attempt.
+            foreach (var item in _notFinalizedBuffer)
             {
                 if (_shouldEnqueue(item.Key))
                 {
@@ -74,6 +81,8 @@ namespace Avalonia.Layout
                     _inner.Enqueue(item.Key);
                 }
             }
+
+            _notFinalizedBuffer.Clear();
         }
     }
 }

+ 39 - 12
src/Avalonia.Layout/Layoutable.cs

@@ -518,17 +518,25 @@ namespace Avalonia.Layout
                 var width = measured.Width;
                 var height = measured.Height;
 
-                if (!double.IsNaN(Width))
                 {
-                    width = Width;
+                    double widthCache = Width;
+
+                    if (!double.IsNaN(widthCache))
+                    {
+                        width = widthCache;
+                    }
                 }
 
                 width = Math.Min(width, MaxWidth);
                 width = Math.Max(width, MinWidth);
 
-                if (!double.IsNaN(Height))
                 {
-                    height = Height;
+                    double heightCache = Height;
+
+                    if (!double.IsNaN(heightCache))
+                    {
+                        height = heightCache;
+                    }
                 }
 
                 height = Math.Min(height, MaxHeight);
@@ -562,11 +570,19 @@ namespace Avalonia.Layout
             double width = 0;
             double height = 0;
 
-            foreach (ILayoutable child in this.GetVisualChildren())
+            var visualChildren = VisualChildren;
+            var visualCount = visualChildren.Count;
+
+            for (var i = 0; i < visualCount; i++)
             {
-                child.Measure(availableSize);
-                width = Math.Max(width, child.DesiredSize.Width);
-                height = Math.Max(height, child.DesiredSize.Height);
+                IVisual visual = visualChildren[i];
+
+                if (visual is ILayoutable layoutable)
+                {
+                    layoutable.Measure(availableSize);
+                    width = Math.Max(width, layoutable.DesiredSize.Width);
+                    height = Math.Max(height, layoutable.DesiredSize.Height);
+                }
             }
 
             return new Size(width, height);
@@ -594,6 +610,7 @@ namespace Avalonia.Layout
                 var verticalAlignment = VerticalAlignment;
                 var size = availableSizeMinusMargins;
                 var scale = GetLayoutScale();
+                var useLayoutRounding = UseLayoutRounding;
 
                 if (horizontalAlignment != HorizontalAlignment.Stretch)
                 {
@@ -607,7 +624,7 @@ namespace Avalonia.Layout
 
                 size = LayoutHelper.ApplyLayoutConstraints(this, size);
 
-                if (UseLayoutRounding)
+                if (useLayoutRounding)
                 {
                     size = new Size(
                         Math.Ceiling(size.Width * scale) / scale, 
@@ -641,7 +658,7 @@ namespace Avalonia.Layout
                         break;
                 }
 
-                if (UseLayoutRounding)
+                if (useLayoutRounding)
                 {
                     originX = Math.Floor(originX * scale) / scale;
                     originY = Math.Floor(originY * scale) / scale;
@@ -658,9 +675,19 @@ namespace Avalonia.Layout
         /// <returns>The actual size used.</returns>
         protected virtual Size ArrangeOverride(Size finalSize)
         {
-            foreach (ILayoutable child in this.GetVisualChildren().OfType<ILayoutable>())
+            var arrangeRect = new Rect(finalSize);
+
+            var visualChildren = VisualChildren;
+            var visualCount = visualChildren.Count;
+
+            for (var i = 0; i < visualCount; i++)
             {
-                child.Arrange(new Rect(finalSize));
+                IVisual visual = visualChildren[i];
+
+                if (visual is ILayoutable layoutable)
+                {
+                    layoutable.Arrange(arrangeRect);
+                }
             }
 
             return finalSize;

+ 1 - 0
src/Avalonia.Native/Avalonia.Native.csproj

@@ -22,5 +22,6 @@
     <PackageReference Include="SharpGenTools.Sdk" Version="1.1.2" PrivateAssets="all" />
     <PackageReference Include="SharpGen.Runtime.COM" Version="1.1.0" />
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
   </ItemGroup>
 </Project>

+ 32 - 2
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@@ -3,12 +3,15 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Linq;
 using System.Text;
+using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Native.Interop;
 using Avalonia.Platform.Interop;
 using Avalonia.Threading;
+using Avalonia.Dialogs;
+using Avalonia.Controls.ApplicationLifetimes;
 
 namespace Avalonia.Native
 {
@@ -211,6 +214,29 @@ namespace Avalonia.Native
             DoLayoutReset();
         }
 
+        private static NativeMenu CreateDefaultAppMenu()
+        {
+            var result = new NativeMenu();
+
+            var aboutItem = new NativeMenuItem
+            {
+                Header = "About Avalonia",
+            };
+
+            aboutItem.Clicked += async (sender, e) =>
+            {
+                var dialog = new AboutAvaloniaDialog();
+
+                var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+
+                await dialog.ShowDialog(mainWindow);
+            };
+
+            result.Add(aboutItem);
+
+            return result;
+        }
+
         private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
         {
             QueueReset();
@@ -241,6 +267,10 @@ namespace Avalonia.Native
                 {
                     SetMenu(_menu);
                 }
+                else
+                {
+                    SetMenu(CreateDefaultAppMenu());
+                }
             }
             else
             {
@@ -321,7 +351,7 @@ namespace Avalonia.Native
                     }), new MenuActionCallback(() => { item.RaiseClick(); }));
                     menu.AddItem(menuItem);
 
-                    if (item.Menu?.Items?.Count > 0)
+                    if (item.Menu?.Items?.Count >= 0)
                     {
                         var submenu = _factory.CreateMenu();
 
@@ -362,7 +392,7 @@ namespace Avalonia.Native
                         return false;
                     }), new MenuActionCallback(() => { item.RaiseClick(); }));
 
-                    if (item.Menu?.Items.Count > 0 || isMainMenu)
+                    if (item.Menu?.Items.Count >= 0 || isMainMenu)
                     {
                         var subMenu = CreateSubmenu(item.Menu?.Items);
 

+ 0 - 2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -21,7 +21,6 @@ namespace Avalonia.Native
         [DllImport("libAvaloniaNative")]
         static extern IntPtr CreateAvaloniaNative();
 
-        internal static readonly MouseDevice MouseDevice = new MouseDevice();
         internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
 
         public Size DoubleClickSize => new Size(4, 4);
@@ -95,7 +94,6 @@ namespace Avalonia.Native
                 .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IMouseDevice>().ToConstant(MouseDevice)
                 .Bind<IPlatformSettings>().ToConstant(this)
                 .Bind<IWindowingPlatform>().ToConstant(this)
                 .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))

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

@@ -24,7 +24,7 @@ namespace Avalonia.Native
         private object _syncRoot = new object();
         private bool _deferredRendering = false;
         private bool _gpu = false;
-        private readonly IMouseDevice _mouse;
+        private readonly MouseDevice _mouse;
         private readonly IKeyboardDevice _keyboard;
         private readonly IStandardCursorFactory _cursorFactory;
         private Size _savedLogicalSize;
@@ -38,7 +38,7 @@ namespace Avalonia.Native
             _deferredRendering = opts.UseDeferredRendering;
 
             _keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
-            _mouse = AvaloniaLocator.Current.GetService<IMouseDevice>();
+            _mouse = new MouseDevice();
             _cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
         }
 
@@ -96,7 +96,7 @@ namespace Avalonia.Native
         public Action<Rect> Paint { get; set; }
         public Action<Size> Resized { get; set; }
         public Action Closed { get; set; }
-        public IMouseDevice MouseDevice => AvaloniaNativePlatform.MouseDevice;
+        public IMouseDevice MouseDevice => _mouse;
         public abstract IPopupImpl CreatePopup();
 
 
@@ -142,6 +142,7 @@ namespace Avalonia.Native
                 {
                     n?.Dispose();
                 }
+                _parent._mouse.Dispose();
             }
 
             void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();

+ 15 - 43
src/Avalonia.Themes.Default/GridSplitter.xaml

@@ -1,51 +1,23 @@
 <Styles xmlns="https://github.com/avaloniaui">
-  <Style Selector="GridSplitter:vertical">
-    <Setter Property="Width" Value="6"/>
-    <Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
-    <Setter Property="Template">
-      <ControlTemplate>
-        <Border Background="{TemplateBinding Background}">
-          <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
-            <StackPanel.Styles>
-              <Style Selector="Ellipse">
-                <Setter Property="HorizontalAlignment" Value="Center"/>
-                <Setter Property="Width" Value="4"/>
-                <Setter Property="Height" Value="4"/>
-                <Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
-                <Setter Property="Margin" Value="1"/>
-              </Style>
-            </StackPanel.Styles>
-            <Ellipse/>
-            <Ellipse/>
-            <Ellipse/>
-          </StackPanel>
-        </Border>
-      </ControlTemplate>
+
+  <Style Selector="GridSplitter">
+    <Setter Property="Focusable" Value="True" />
+    <Setter Property="MinWidth" Value="6" />
+    <Setter Property="MinHeight" Value="6" />
+    <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
+    <Setter Property="PreviewContent">
+      <Template>
+        <Rectangle Fill="{DynamicResource HighlightBrush}" />
+      </Template>
     </Setter>
-  </Style>
-  <Style Selector="GridSplitter:horizontal">
-    <Setter Property="Height" Value="6"/>
-    <Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Background="{TemplateBinding Background}">
-          <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
-            <StackPanel.Styles>
-              <Style Selector="Ellipse">
-                <Setter Property="VerticalAlignment" Value="Center"/>
-                <Setter Property="Width" Value="4"/>
-                <Setter Property="Height" Value="4"/>
-                <Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
-                <Setter Property="Margin" Value="1"/>
-              </Style>
-            </StackPanel.Styles>
-            <Ellipse/>
-            <Ellipse/>
-            <Ellipse/>
-          </StackPanel>
-        </Border>
+        <Border 
+          BorderBrush="{TemplateBinding BorderBrush}"
+          BorderThickness="{TemplateBinding BorderThickness}"
+          Background="{TemplateBinding Background}"/>
       </ControlTemplate>
     </Setter>
   </Style>
-</Styles>
 
+</Styles>

+ 20 - 8
src/Avalonia.Visuals/Media/FontFamily.cs

@@ -5,12 +5,16 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Media.Fonts;
-using Avalonia.Platform;
 
 namespace Avalonia.Media
 {
-    public class FontFamily
+    public sealed class FontFamily
     {
+        static FontFamily()
+        {
+            Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
+        }
+
         /// <inheritdoc />
         /// <summary>
         /// Initializes a new instance of the <see cref="T:Avalonia.Media.FontFamily" /> class.
@@ -30,9 +34,7 @@ namespace Avalonia.Media
         {
             if (string.IsNullOrEmpty(name))
             {
-                FamilyNames = new FamilyNameCollection(string.Empty);
-
-                return;
+                throw new ArgumentNullException(nameof(name));
             }
 
             var fontFamilySegment = GetFontFamilyIdentifier(name);
@@ -53,13 +55,16 @@ namespace Avalonia.Media
         /// <summary>
         /// Represents the default font family
         /// </summary>
-        public static FontFamily Default => new FontFamily(string.Empty);
+        public static FontFamily Default { get; }
 
         /// <summary>
         /// Represents all font families in the system. This can be an expensive call depending on platform implementation.
         /// </summary>
+        /// <remarks>
+        /// Consider using the new <see cref="FontManager"/> instead.
+        /// </remarks>
         public static IEnumerable<FontFamily> SystemFontFamilies =>
-            AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().InstalledFontNames.Select(name => new FontFamily(name));
+            FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
 
         /// <summary>
         /// Gets the primary family name of the font family.
@@ -181,7 +186,14 @@ namespace Avalonia.Media
             {
                 var hash = (int)2186146271;
 
-                hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+                if (Key != null)
+                {
+                    hash = (hash * 15768619) ^ Key.GetHashCode();
+                }
+                else
+                {
+                    hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+                }
 
                 if (Key != null)
                 {

+ 112 - 0
src/Avalonia.Visuals/Media/FontManager.cs

@@ -0,0 +1,112 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    ///     The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
+    ///     It is also responsible for the font fallback.
+    /// </summary>
+    public abstract class FontManager
+    {
+        public static readonly FontManager Default = CreateDefault();
+
+        /// <summary>
+        ///     Gets the system's default font family's name.
+        /// </summary>
+        public string DefaultFontFamilyName
+        {
+            get;
+            protected set;
+        }
+
+        /// <summary>
+        ///     Get all installed fonts in the system.
+        /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
+        /// </summary>
+        public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
+
+        /// <summary>
+        ///     Get a cached typeface from specified parameters.
+        /// </summary>
+        /// <param name="fontFamily">The font family.</param>
+        /// <param name="fontWeight">The font weight.</param>
+        /// <param name="fontStyle">The font style.</param>
+        /// <returns>
+        ///     The cached typeface.
+        /// </returns>
+        public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+
+        /// <summary>
+        ///     Tries to match a specified character to a typeface that supports specified font properties.
+        ///     Returns <c>null</c> if no fallback was found.
+        /// </summary>
+        /// <param name="codepoint">The codepoint to match against.</param>
+        /// <param name="fontWeight">The font weight.</param>
+        /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
+        /// <param name="culture">The culture.</param>
+        /// <returns>
+        ///     The matched typeface.
+        /// </returns>
+        public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+            FontStyle fontStyle = default,
+            FontFamily fontFamily = null, CultureInfo culture = null);
+
+        public static FontManager CreateDefault()
+        {
+            var platformImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
+
+            if (platformImpl != null)
+            {
+                return new PlatformFontManager(platformImpl);
+            }
+
+            return new EmptyFontManager();
+        }
+
+        private class PlatformFontManager : FontManager
+        {
+            private readonly IFontManagerImpl _platformImpl;
+
+            public PlatformFontManager(IFontManagerImpl platformImpl)
+            {
+                _platformImpl = platformImpl;
+
+                DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
+            }
+
+            public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+                _platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+
+            public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
+                _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
+
+            public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+                FontStyle fontStyle = default,
+                FontFamily fontFamily = null, CultureInfo culture = null) =>
+                _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
+        }
+
+        private class EmptyFontManager : FontManager
+        {
+            public EmptyFontManager()
+            {
+                DefaultFontFamilyName = "Empty";
+            }
+
+            public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+                new[] { DefaultFontFamilyName };
+
+            public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
+
+            public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+                FontStyle fontStyle = default,
+                FontFamily fontFamily = null, CultureInfo culture = null) => null;
+        }
+    }
+}

+ 5 - 1
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@@ -9,7 +9,7 @@ using System.Text;
 
 namespace Avalonia.Media.Fonts
 {
-    public class FamilyNameCollection : IEnumerable<string>
+    public sealed class FamilyNameCollection : IReadOnlyList<string>
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
@@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
 
             return other.ToString().Equals(ToString());
         }
+
+        public int Count => Names.Count;
+
+        public string this[int index] => Names[index];
     }
 }

+ 42 - 5
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -16,9 +16,10 @@ namespace Avalonia.Media
         private IFormattedTextImpl _platformImpl;
         private IReadOnlyList<FormattedTextStyleSpan> _spans;
         private Typeface _typeface;
+        private double _fontSize;
         private string _text;
         private TextAlignment _textAlignment;
-        private TextWrapping _wrapping;
+        private TextWrapping _textWrapping;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FormattedText"/> class.
@@ -37,6 +38,31 @@ namespace Avalonia.Media
             _platform = platform;
         }
 
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="text"></param>
+        /// <param name="typeface"></param>
+        /// <param name="fontSize"></param>
+        /// <param name="textAlignment"></param>
+        /// <param name="textWrapping"></param>
+        /// <param name="constraint"></param>
+        public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
+            TextWrapping textWrapping, Size constraint)
+        {
+            _text = text;
+
+            _typeface = typeface;
+
+            _fontSize = fontSize;
+
+            _textAlignment = textAlignment;
+
+            _textWrapping = textWrapping;
+
+            _constraint = constraint;
+        }
+
         /// <summary>
         /// Gets the bounds of the text within the <see cref="Constraint"/>.
         /// </summary>
@@ -61,6 +87,16 @@ namespace Avalonia.Media
             set => Set(ref _typeface, value);
         }
 
+
+        /// <summary>
+        /// Gets or sets the font size.
+        /// </summary>
+        public double FontSize
+        {
+            get => _fontSize;
+            set => Set(ref _fontSize, value);
+        }
+
         /// <summary>
         /// Gets or sets a collection of spans that describe the formatting of subsections of the
         /// text.
@@ -92,10 +128,10 @@ namespace Avalonia.Media
         /// <summary>
         /// Gets or sets the text wrapping.
         /// </summary>
-        public TextWrapping Wrapping
+        public TextWrapping TextWrapping
         {
-            get => _wrapping;
-            set => Set(ref _wrapping, value);
+            get => _textWrapping;
+            set => Set(ref _textWrapping, value);
         }
 
         /// <summary>
@@ -110,8 +146,9 @@ namespace Avalonia.Media
                     _platformImpl = _platform.CreateFormattedText(
                         _text,
                         _typeface,
+                        _fontSize,
                         _textAlignment,
-                        _wrapping,
+                        _textWrapping,
                         _constraint,
                         _spans);
                 }

+ 111 - 0
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@@ -0,0 +1,111 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+    public sealed class GlyphTypeface : IDisposable
+    {
+        private static readonly IPlatformRenderInterface s_platformRenderInterface =
+            AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+
+        public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
+        {
+        }
+
+        public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
+        {
+            PlatformImpl = platformImpl;
+        }
+
+        public IGlyphTypefaceImpl PlatformImpl { get; }
+
+        /// <summary>
+        ///     Gets the font design units per em.
+        /// </summary>
+        public short DesignEmHeight => PlatformImpl.DesignEmHeight;
+
+        /// <summary>
+        ///     Gets the recommended distance above the baseline in design em size. 
+        /// </summary>
+        public int Ascent => PlatformImpl.Ascent;
+
+        /// <summary>
+        ///     Gets the recommended distance under the baseline in design em size. 
+        /// </summary>
+        public int Descent => PlatformImpl.Descent;
+
+        /// <summary>
+        ///      Gets the recommended additional space between two lines of text in design em size. 
+        /// </summary>
+        public int LineGap => PlatformImpl.LineGap;
+
+        /// <summary>
+        ///     Gets the recommended line height.
+        /// </summary>
+        public int LineHeight => Descent - Ascent + LineGap;
+
+        /// <summary>
+        ///     Gets a value that indicates the distance of the underline from the baseline in design em size.
+        /// </summary>
+        public int UnderlinePosition => PlatformImpl.UnderlinePosition;
+
+        /// <summary>
+        ///     Gets a value that indicates the thickness of the underline in design em size.
+        /// </summary>
+        public int UnderlineThickness => PlatformImpl.UnderlineThickness;
+
+        /// <summary>
+        ///     Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+        /// </summary>
+        public int StrikethroughPosition => PlatformImpl.StrikethroughPosition;
+
+        /// <summary>
+        ///     Gets a value that indicates the thickness of the underline in design em size.
+        /// </summary>
+        public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
+
+        /// <summary>
+        ///     Returns an glyph index for the specified codepoint.
+        /// </summary>
+        /// <remarks>
+        ///     Returns <c>0</c> if a glyph isn't found.
+        /// </remarks>
+        /// <param name="codepoint">The codepoint.</param>
+        /// <returns>
+        ///     A glyph index.
+        /// </returns>
+        public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
+
+        /// <summary>
+        ///     Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
+        /// </summary>
+        /// <param name="codepoints">The codepoints to map.</param>
+        /// <returns></returns>
+        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) => PlatformImpl.GetGlyphs(codepoints);
+
+        /// <summary>
+        ///     Returns the glyph advance for the specified glyph.
+        /// </summary>
+        /// <param name="glyph">The glyph.</param>
+        /// <returns>
+        ///     The advance.
+        /// </returns>
+        public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph);
+
+        /// <summary>
+        ///     Returns an array of glyph advances in design em size.
+        /// </summary>
+        /// <param name="glyphs">The glyph indices.</param>
+        /// <returns></returns>
+        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) => PlatformImpl.GetGlyphAdvances(glyphs);
+
+        void IDisposable.Dispose()
+        {
+            PlatformImpl?.Dispose();
+        }
+    }
+}

+ 72 - 27
src/Avalonia.Visuals/Media/Typeface.cs

@@ -1,39 +1,38 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+using JetBrains.Annotations;
 
 namespace Avalonia.Media
 {
     /// <summary>
     /// Represents a typeface.
     /// </summary>
-    public class Typeface
+    [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
+    public class Typeface : IEquatable<Typeface>
     {
         public static readonly Typeface Default = new Typeface(FontFamily.Default);
 
+        private GlyphTypeface _glyphTypeface;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Typeface"/> class.
         /// </summary>
         /// <param name="fontFamily">The font family.</param>
-        /// <param name="fontSize">The font size, in DIPs.</param>
-        /// <param name="style">The font style.</param>
         /// <param name="weight">The font weight.</param>
-        public Typeface(
-            FontFamily fontFamily, 
-            double fontSize = 12, 
-            FontStyle style = FontStyle.Normal,
-            FontWeight weight = FontWeight.Normal)
+        /// <param name="style">The font style.</param>
+        public Typeface([NotNull]FontFamily fontFamily,
+            FontWeight weight = FontWeight.Normal,
+            FontStyle style = FontStyle.Normal)
         {
-            if (fontSize <= 0)
-            {
-                throw new ArgumentException("Font size must be > 0.");
-            }
-
             if (weight <= 0)
             {
                 throw new ArgumentException("Font weight must be > 0.");
             }
 
             FontFamily = fontFamily;
-            FontSize = fontSize;
             Style = style;
             Weight = weight;
         }
@@ -42,15 +41,12 @@ namespace Avalonia.Media
         /// Initializes a new instance of the <see cref="Typeface"/> class.
         /// </summary>
         /// <param name="fontFamilyName">The name of the font family.</param>
-        /// <param name="fontSize">The font size, in DIPs.</param>
         /// <param name="style">The font style.</param>
         /// <param name="weight">The font weight.</param>
-        public Typeface(
-            string fontFamilyName,
-            double fontSize = 12,
-            FontStyle style = FontStyle.Normal,
-            FontWeight weight = FontWeight.Normal)
-            : this(new FontFamily(fontFamilyName), fontSize, style, weight)
+        public Typeface(string fontFamilyName,
+            FontWeight weight = FontWeight.Normal,
+            FontStyle style = FontStyle.Normal)
+            : this(new FontFamily(fontFamilyName), weight, style)
         {
         }
 
@@ -59,11 +55,6 @@ namespace Avalonia.Media
         /// </summary>
         public FontFamily FontFamily { get; }
 
-        /// <summary>
-        /// Gets the size of the font in DIPs.
-        /// </summary>
-        public double FontSize { get; }
-
         /// <summary>
         /// Gets the font style.
         /// </summary>
@@ -73,5 +64,59 @@ namespace Avalonia.Media
         /// Gets the font weight.
         /// </summary>
         public FontWeight Weight { get; }
+
+        /// <summary>
+        /// Gets the glyph typeface.
+        /// </summary>
+        /// <value>
+        /// The glyph typeface.
+        /// </value>
+        public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this));
+
+        public static bool operator !=(Typeface a, Typeface b)
+        {
+            return !(a == b);
+        }
+
+        public static bool operator ==(Typeface a, Typeface b)
+        {
+            if (ReferenceEquals(a, b))
+            {
+                return true;
+            }
+
+            return !(a is null) && a.Equals(b);
+        }
+
+        public override bool Equals(object obj)
+        {
+            if (obj is Typeface typeface)
+            {
+                return Equals(typeface);
+            }
+
+            return false;
+        }
+
+        public bool Equals(Typeface other)
+        {
+            if (other is null)
+            {
+                return false;
+            }
+
+            return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0);
+                hashCode = (hashCode * 397) ^ (int)Style;
+                hashCode = (hashCode * 397) ^ (int)Weight;
+                return hashCode;
+            }
+        }
     }
 }

+ 48 - 0
src/Avalonia.Visuals/Platform/IFontManagerImpl.cs

@@ -0,0 +1,48 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+
+namespace Avalonia.Platform
+{
+    public interface IFontManagerImpl
+    {
+        /// <summary>
+        ///     Gets the system's default font family's name.
+        /// </summary>
+        string DefaultFontFamilyName { get; }
+
+        /// <summary>
+        ///     Get all installed fonts in the system.
+        /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
+        /// </summary>
+        IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
+
+        /// <summary>
+        ///     Get a typeface from specified parameters.
+        /// </summary>
+        /// <param name="fontFamily">The font family.</param>
+        /// <param name="fontWeight">The font weight.</param>
+        /// <param name="fontStyle">The font style.</param>
+        /// <returns>
+        ///     The typeface.
+        /// </returns>
+        Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+
+        /// <summary>
+        ///     Tries to match a specified character to a typeface that supports specified font properties.
+        /// </summary>
+        /// <param name="codepoint">The codepoint to match against.</param>
+        /// <param name="fontWeight">The font weight.</param>
+        /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
+        /// <param name="culture">The culture.</param>
+        /// <returns>
+        ///     The typeface.
+        /// </returns>
+        Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+            FontFamily fontFamily = null, CultureInfo culture = null);
+    }
+}

+ 89 - 0
src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs

@@ -0,0 +1,89 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Platform
+{
+    public interface IGlyphTypefaceImpl : IDisposable
+    {
+        /// <summary>
+        ///     Gets the font design units per em.
+        /// </summary>
+        short DesignEmHeight { get; }
+
+        /// <summary>
+        ///     Gets the recommended distance above the baseline in design em size. 
+        /// </summary>
+        int Ascent { get; }
+
+        /// <summary>
+        ///     Gets the recommended distance under the baseline in design em size. 
+        /// </summary>
+        int Descent { get; }
+
+        /// <summary>
+        ///      Gets the recommended additional space between two lines of text in design em size. 
+        /// </summary>
+        int LineGap { get; }
+
+        /// <summary>
+        ///     Gets a value that indicates the distance of the underline from the baseline in design em size.
+        /// </summary>
+        int UnderlinePosition { get; }
+
+        /// <summary>
+        ///     Gets a value that indicates the thickness of the underline in design em size.
+        /// </summary>
+        int UnderlineThickness { get; }
+
+        /// <summary>
+        ///     Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+        /// </summary>
+        int StrikethroughPosition { get; }
+
+        /// <summary>
+        ///     Gets a value that indicates the thickness of the underline in design em size.
+        /// </summary>
+        int StrikethroughThickness { get; }
+
+        /// <summary>
+        ///     Returns an glyph index for the specified codepoint.
+        /// </summary>
+        /// <remarks>
+        ///     Returns <c>0</c> if a glyph isn't found.
+        /// </remarks>
+        /// <param name="codepoint">The codepoint.</param>
+        /// <returns>
+        ///     A glyph index.
+        /// </returns>
+        ushort GetGlyph(uint codepoint);
+
+        /// <summary>
+        ///     Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
+        /// </summary>
+        /// <param name="codepoints">The codepoints to map.</param>
+        /// <returns>
+        ///     An array of glyph indices.
+        /// </returns>
+        ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints);
+
+        /// <summary>
+        ///     Returns the glyph advance for the specified glyph.
+        /// </summary>
+        /// <param name="glyph">The glyph.</param>
+        /// <returns>
+        ///     The advance.
+        /// </returns>
+        int GetGlyphAdvance(ushort glyph);
+
+        /// <summary>
+        ///     Returns an array of glyph advances in design em size.
+        /// </summary>
+        /// <param name="glyphs">The glyph indices.</param>
+        /// <returns>
+        ///     An array of glyph advances.
+        /// </returns>
+        int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs);
+    }
+}

+ 11 - 5
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -13,16 +13,12 @@ namespace Avalonia.Platform
     /// </summary>
     public interface IPlatformRenderInterface
     {
-        /// <summary>
-        /// Get all installed fonts in the system
-        /// </summary>
-        IEnumerable<string> InstalledFontNames { get; }
-
         /// <summary>
         /// Creates a formatted text implementation.
         /// </summary>
         /// <param name="text">The text.</param>
         /// <param name="typeface">The base typeface.</param>
+        /// <param name="fontSize">The font size.</param>
         /// <param name="textAlignment">The text alignment.</param>
         /// <param name="wrapping">The text wrapping mode.</param>
         /// <param name="constraint">The text layout constraints.</param>
@@ -31,6 +27,7 @@ namespace Avalonia.Platform
         IFormattedTextImpl CreateFormattedText(
             string text,
             Typeface typeface,
+            double fontSize,
             TextAlignment textAlignment,
             TextWrapping wrapping,
             Size constraint,
@@ -114,5 +111,14 @@ namespace Avalonia.Platform
         /// <param name="stride">The number of bytes per row.</param>
         /// <returns>An <see cref="IBitmapImpl"/>.</returns>
         IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
+
+        /// <summary>
+        ///     Creates a glyph typeface for specified typeface.
+        /// </summary>
+        /// <param name="typeface">The typeface.</param>
+        /// <returns>
+        ///     The glyph typeface implementation.
+        /// </returns>
+        IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
     }
 }

+ 4 - 2
src/Avalonia.Visuals/Rendering/RendererBase.cs

@@ -7,7 +7,8 @@ namespace Avalonia.Rendering
 {
     public class RendererBase
     {
-        private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
+        private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
+        private static int s_fontSize = 18;
         private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
         private int _framesThisSecond;
         private int _fps;
@@ -18,7 +19,8 @@ namespace Avalonia.Rendering
         {
             _fpsText = new FormattedText
             {
-                Typeface = s_fpsTypeface
+                Typeface = s_fpsTypeface,
+                FontSize = s_fontSize
             };
         }
 

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph
                 UpdateSize(scene);
             }
 
-            if (visual.VisualRoot != null)
+            if (visual.VisualRoot == scene.Root.Visual)
             {
                 if (node?.Parent != null &&
                     visual.VisualParent != null &&

+ 0 - 2
src/Avalonia.X11/X11Platform.cs

@@ -19,9 +19,7 @@ namespace Avalonia.X11
     class AvaloniaX11Platform : IWindowingPlatform
     {
         private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
-        private Lazy<MouseDevice> _mouseDevice = new Lazy<MouseDevice>(() => new MouseDevice());
         public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
-        public MouseDevice MouseDevice => _mouseDevice.Value;
         public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
         public XI2Manager XI2;
         public X11Info Info { get; private set; }

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

@@ -32,7 +32,8 @@ namespace Avalonia.X11
         private PixelPoint? _configurePoint;
         private bool _triggeredExpose;
         private IInputRoot _inputRoot;
-        private readonly IMouseDevice _mouse;
+        private readonly MouseDevice _mouse;
+        private readonly TouchDevice _touch;
         private readonly IKeyboardDevice _keyboard;
         private PixelPoint? _position;
         private PixelSize _realSize;
@@ -57,7 +58,8 @@ namespace Avalonia.X11
             _platform = platform;
             _popup = popupParent != null;
             _x11 = platform.Info;
-            _mouse = platform.MouseDevice;
+            _mouse = new MouseDevice();
+            _touch = new TouchDevice();
             _keyboard = platform.KeyboardDevice;
 
             var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
@@ -702,6 +704,8 @@ namespace Avalonia.X11
                 _platform.XI2?.OnWindowDestroyed(_handle);
                 _handle = IntPtr.Zero;
                 Closed?.Invoke();
+                _mouse.Dispose();
+                _touch.Dispose();
             }
             
             if (_useRenderWindow && _renderHandle != IntPtr.Zero)
@@ -830,6 +834,8 @@ namespace Avalonia.X11
         }
 
         public IMouseDevice MouseDevice => _mouse;
+        public TouchDevice TouchDevice => _touch;
+
         public IPopupImpl CreatePopup() 
             => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);
 

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

@@ -92,8 +92,6 @@ namespace Avalonia.X11
         
         private PointerDeviceInfo _pointerDevice;
         private AvaloniaX11Platform _platform;
-        private readonly TouchDevice _touchDevice = new TouchDevice();
-
 
         public bool Init(AvaloniaX11Platform platform)
         {
@@ -198,7 +196,7 @@ namespace Avalonia.X11
                     (ev.Type == XiEventType.XI_TouchUpdate ?
                         RawPointerEventType.TouchUpdate :
                         RawPointerEventType.TouchEnd);
-                client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
+                client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
                     ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
                 return;
             }
@@ -232,10 +230,10 @@ namespace Avalonia.X11
                 }
 
                 if (scrollDelta != default)
-                    client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp,
+                    client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
                         client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
                 if (_pointerDevice.HasMotion(ev))
-                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         RawPointerEventType.Move, ev.Position, ev.Modifiers));
             }
 
@@ -248,7 +246,7 @@ namespace Avalonia.X11
                     : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
                     : (RawPointerEventType?)null;
                 if (type.HasValue)
-                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
                         type.Value, ev.Position, ev.Modifiers));
             }
             
@@ -310,5 +308,7 @@ namespace Avalonia.X11
     {
         IInputRoot InputRoot { get; }
         void ScheduleInput(RawInputEventArgs args);
+        IMouseDevice MouseDevice { get; }
+        TouchDevice TouchDevice { get; }
     }
 }

+ 1 - 0
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@@ -12,5 +12,6 @@
   </ItemGroup>
   
   <Import Project="..\..\..\build\SkiaSharp.props" />
+  <Import Project="..\..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
 </Project>

+ 40 - 0
src/Skia/Avalonia.Skia/FontKey.cs

@@ -0,0 +1,40 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Skia
+{
+    internal readonly struct FontKey : IEquatable<FontKey>
+    {
+        public readonly FontStyle Style;
+        public readonly FontWeight Weight;
+
+        public FontKey(FontWeight weight, FontStyle style)
+        {
+            Style = style;
+            Weight = weight;
+        }
+
+        public override int GetHashCode()
+        {
+            var hash = 17;
+            hash = hash * 31 + (int)Style;
+            hash = hash * 31 + (int)Weight;
+
+            return hash;
+        }
+
+        public override bool Equals(object other)
+        {
+            return other is FontKey key && Equals(key);
+        }
+
+        public bool Equals(FontKey other)
+        {
+            return Style == other.Style &&
+                   Weight == other.Weight;
+        }
+    }
+}

+ 82 - 0
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -0,0 +1,82 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    internal class FontManagerImpl : IFontManagerImpl
+    {
+        private SKFontManager _skFontManager = SKFontManager.Default;
+
+        public FontManagerImpl()
+        {
+            DefaultFontFamilyName = SKTypeface.Default.FamilyName;
+        }
+
+        public string DefaultFontFamilyName { get; }
+
+        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        {
+            if (checkForUpdates)
+            {
+                _skFontManager = SKFontManager.CreateDefault();
+            }
+
+            return _skFontManager.FontFamilies;
+        }
+
+        public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+        {
+            return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
+        }
+
+        public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+            FontFamily fontFamily = null, CultureInfo culture = null)
+        {
+            var fontFamilyName = FontFamily.Default.Name;
+
+            if (culture == null)
+            {
+                culture = CultureInfo.CurrentUICulture;
+            }
+
+            if (fontFamily != null)
+            {
+                foreach (var familyName in fontFamily.FamilyNames)
+                {
+                    var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
+                        SKFontStyleWidth.Normal,
+                        (SKFontStyleSlant)fontStyle,
+                        new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+
+                    if (skTypeface == null)
+                    {
+                        continue;
+                    }
+
+                    fontFamilyName = familyName;
+
+                    break;
+                }
+            }
+            else
+            {
+                var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
+                    (SKFontStyleSlant)fontStyle,
+                    new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+
+                if (skTypeface != null)
+                {
+                    fontFamilyName = skTypeface.FamilyName;
+                }
+            }
+
+            return GetTypeface(fontFamilyName, fontWeight, fontStyle);
+        }
+    }
+}

+ 38 - 42
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -18,6 +18,7 @@ namespace Avalonia.Skia
         public FormattedTextImpl(
             string text,
             Typeface typeface,
+            double fontSize,
             TextAlignment textAlignment,
             TextWrapping wrapping,
             Size constraint,
@@ -28,47 +29,22 @@ namespace Avalonia.Skia
             // Replace 0 characters with zero-width spaces (200B)
             Text = Text.Replace((char)0, (char)0x200B);
 
-            SKTypeface skiaTypeface = null;
+            var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
 
-            if (typeface.FontFamily.Key != null)
+            _paint = new SKPaint
             {
-                var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
-                skiaTypeface = typefaces.GetTypeFace(typeface);
-            }
-            else
-            {
-                if (typeface.FontFamily.FamilyNames.HasFallbacks)
-                {
-                    foreach (var familyName in typeface.FontFamily.FamilyNames)
-                    {
-                        skiaTypeface = TypefaceCache.GetTypeface(
-                            familyName,
-                            typeface.Style,
-                            typeface.Weight);
-                        if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break;
-                    }
-                }
-                else
-                {
-                    skiaTypeface = TypefaceCache.GetTypeface(
-                        typeface.FontFamily.Name,
-                        typeface.Style,
-                        typeface.Weight);
-                }
-            }
-
-            _paint = new SKPaint();
+                TextEncoding = SKTextEncoding.Utf16,
+                IsStroke = false,
+                IsAntialias = true,
+                LcdRenderText = true,
+                SubpixelText = true,
+                Typeface = entry.SKTypeface,
+                TextSize = (float)fontSize,
+                TextAlign = textAlignment.ToSKTextAlign()
+            };
 
             //currently Skia does not measure properly with Utf8 !!!
             //Paint.TextEncoding = SKTextEncoding.Utf8;
-            _paint.TextEncoding = SKTextEncoding.Utf16;
-            _paint.IsStroke = false;
-            _paint.IsAntialias = true;
-            _paint.LcdRenderText = true;
-            _paint.SubpixelText = true;
-            _paint.Typeface = skiaTypeface;
-            _paint.TextSize = (float)typeface.FontSize;
-            _paint.TextAlign = textAlignment.ToSKTextAlign();
 
             _wrapping = wrapping;
             _constraint = constraint;
@@ -99,7 +75,24 @@ namespace Avalonia.Skia
         public TextHitTestResult HitTestPoint(Point point)
         {
             float y = (float)point.Y;
-            var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
+
+            AvaloniaFormattedTextLine line = default;
+
+            float nextTop = 0;
+
+            foreach(var currentLine in _skiaLines)
+            {
+                if(currentLine.Top <= y)
+                {
+                    line = currentLine;
+                    nextTop = currentLine.Top + currentLine.Height;
+                }
+                else
+                {
+                    nextTop = currentLine.Top;
+                    break;
+                }
+            }
 
             if (!line.Equals(default(AvaloniaFormattedTextLine)))
             {
@@ -127,12 +120,15 @@ namespace Avalonia.Skia
                                     line.Length : (line.Length - 1);
                 }
 
-                return new TextHitTestResult
+                if (y < nextTop)
                 {
-                    IsInside = false,
-                    TextPosition = line.Start + offset,
-                    IsTrailing = Text.Length == (line.Start + offset + 1)
-                };
+                    return new TextHitTestResult
+                    {
+                        IsInside = false,
+                        TextPosition = line.Start + offset,
+                        IsTrailing = Text.Length == (line.Start + offset + 1)
+                    };
+                }
             }
 
             bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);

+ 179 - 0
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -0,0 +1,179 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    public class GlyphTypefaceImpl : IGlyphTypefaceImpl
+    {
+        private bool _isDisposed;
+
+        public GlyphTypefaceImpl(Typeface typeface)
+        {
+            Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
+
+            Face = new Face(GetTable)
+            {
+                UnitsPerEm = Typeface.UnitsPerEm
+            };
+
+            Font = new Font(Face);
+
+            Font.SetFunctionsOpenType();
+
+            Font.GetScale(out var xScale, out _);
+
+            DesignEmHeight = (short)xScale;
+
+            if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
+            {
+                Font.TryGetVerticalFontExtents(out fontExtents);
+            }
+
+            Ascent = -fontExtents.Ascender;
+
+            Descent = -fontExtents.Descender;
+
+            LineGap = fontExtents.LineGap;
+
+            if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
+            {
+                UnderlinePosition = underlinePosition;
+            }
+
+            if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
+            {
+                UnderlineThickness = underlineThickness;
+            }
+
+            if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
+            {
+                StrikethroughPosition = strikethroughPosition;
+            }
+
+            if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
+            {
+                StrikethroughThickness = strikethroughThickness;
+            }
+        }
+
+        public Face Face { get; }
+
+        public Font Font { get; }
+
+        public SKTypeface Typeface { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public short DesignEmHeight { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int Ascent { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int Descent { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int LineGap { get; }
+
+        //ToDo: Get these values from HarfBuzz
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int UnderlinePosition { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int UnderlineThickness { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int StrikethroughPosition { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int StrikethroughThickness { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public ushort GetGlyph(uint codepoint)
+        {
+            if (Font.TryGetGlyph(codepoint, out var glyph))
+            {
+                return (ushort)glyph;
+            }
+
+            return 0;
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
+        {
+            var glyphs = new ushort[codepoints.Length];
+
+            for (var i = 0; i < codepoints.Length; i++)
+            {
+                if (Font.TryGetGlyph(codepoints[i], out var glyph))
+                {
+                    glyphs[i] = (ushort)glyph;
+                }
+            }
+
+            return glyphs;
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int GetGlyphAdvance(ushort glyph)
+        {
+            return Font.GetHorizontalGlyphAdvance(glyph);
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
+        {
+            var glyphIndices = new uint[glyphs.Length];
+
+            for (var i = 0; i < glyphs.Length; i++)
+            {
+                glyphIndices[i] = glyphs[i];
+            }
+
+            return Font.GetHorizontalGlyphAdvances(glyphIndices);
+        }
+
+        private Blob GetTable(Face face, Tag tag)
+        {
+            var size = Typeface.GetTableSize(tag);
+
+            var data = Marshal.AllocCoTaskMem(size);
+
+            var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
+
+            return Typeface.TryGetTableData(tag, 0, size, data) ?
+                new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
+        }
+
+        private void Dispose(bool disposing)
+        {
+            if (_isDisposed)
+            {
+                return;
+            }
+
+            _isDisposed = true;
+
+            if (!disposing)
+            {
+                return;
+            }
+
+            Font?.Dispose();
+            Face?.Dispose();
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 11 - 3
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using Avalonia.Controls.Platform.Surfaces;
@@ -17,12 +18,13 @@ namespace Avalonia.Skia
     /// </summary>
     internal class PlatformRenderInterface : IPlatformRenderInterface
     {
+        private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
+            new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
+
         private readonly ICustomSkiaGpu _customSkiaGpu;
 
         private GRContext GrContext { get; }
 
-        public IEnumerable<string> InstalledFontNames => SKFontManager.Default.FontFamilies;
-
         public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
         {
             if (customSkiaGpu != null)
@@ -52,12 +54,13 @@ namespace Avalonia.Skia
         public IFormattedTextImpl CreateFormattedText(
             string text,
             Typeface typeface,
+            double fontSize,
             TextAlignment textAlignment,
             TextWrapping wrapping,
             Size constraint,
             IReadOnlyList<FormattedTextStyleSpan> spans)
         {
-            return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
+            return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
         }
 
         public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@@ -151,5 +154,10 @@ namespace Avalonia.Skia
         {
             return new WriteableBitmapImpl(size, dpi, format);
         }
+
+        public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+        {
+            return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+        }
     }
 }

+ 18 - 73
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -4,114 +4,59 @@
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
-
 using Avalonia.Media;
 
-using SkiaSharp;
-
 namespace Avalonia.Skia
 {
     internal class SKTypefaceCollection
     {
-        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>> _fontFamilies =
-            new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>>();
+        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
+            new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
 
-        public void AddTypeFace(SKTypeface typeface)
+        public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
         {
-            var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant);
-
-            if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily))
+            if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
             {
-                fontFamily = new ConcurrentDictionary<FontKey, SKTypeface>();
+                fontFamily = new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>();
 
-                _fontFamilies.TryAdd(typeface.FamilyName, fontFamily);
+                _fontFamilies.TryAdd(familyName, fontFamily);
             }
 
-            fontFamily.TryAdd(key, typeface);
+            fontFamily.TryAdd(key, entry);
         }
 
-        public SKTypeface GetTypeFace(Typeface typeface)
+        public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
         {
-            var styleSlant = SKFontStyleSlant.Upright;
-
-            switch (typeface.Style)
-            {
-                case FontStyle.Italic:
-                    styleSlant = SKFontStyleSlant.Italic;
-                    break;
-
-                case FontStyle.Oblique:
-                    styleSlant = SKFontStyleSlant.Oblique;
-                    break;
-            }
+            var key = new FontKey(fontWeight, fontStyle);
 
-            if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily))
-            {
-                return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight);
-            }
-
-            var weight = (SKFontStyleWeight)typeface.Weight;
-
-            var key = new FontKey(weight, styleSlant);
-
-            return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key));
+            return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
+                fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
+                new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
         }
 
-        private static SKTypeface GetFallback(IDictionary<FontKey, SKTypeface> fontFamily, FontKey key)
+        private static TypefaceCollectionEntry GetFallback(IDictionary<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
         {
             var keys = fontFamily.Keys.Where(
-                x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray();
+                x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
 
             if (!keys.Any())
             {
                 keys = fontFamily.Keys.Where(
-                    x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray();
+                    x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
 
                 if (!keys.Any())
                 {
                     keys = fontFamily.Keys.Where(
                         x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
-                             (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray();
+                             (x.Style >= key.Style || x.Style < key.Style)).ToArray();
                 }
             }
 
             key = keys.FirstOrDefault();
 
-            fontFamily.TryGetValue(key, out var typeface);
-
-            return typeface;
-        }
-
-        private struct FontKey
-        {
-            public readonly SKFontStyleSlant Slant;
-            public readonly SKFontStyleWeight Weight;
-
-            public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
-            {
-                Slant = slant;
-                Weight = weight;
-            }
-
-            public override int GetHashCode()
-            {
-                var hash = 17;
-                hash = (hash * 31) + (int)Slant;
-                hash = (hash * 31) + (int)Weight;
-
-                return hash;
-            }
+            fontFamily.TryGetValue(key, out var entry);
 
-            public override bool Equals(object other)
-            {
-                return other is FontKey key && this.Equals(key);
-            }
-
-            private bool Equals(FontKey other)
-            {
-                return Slant == other.Slant &&
-                       Weight == other.Weight;
-            }
+            return entry;
         }
     }
 }

+ 6 - 2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@@ -45,9 +45,13 @@ namespace Avalonia.Skia
             {
                 var assetStream = assetLoader.Open(asset);
 
-                var typeface = SKTypeface.FromStream(assetStream);
+                var skTypeface = SKTypeface.FromStream(assetStream);
 
-                typeFaceCollection.AddTypeFace(typeface);
+                var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant);
+
+                var entry = new TypefaceCollectionEntry(typeface, skTypeface);
+
+                typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry);
             }
 
             return typeFaceCollection;

+ 5 - 0
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@@ -25,6 +25,11 @@ namespace Avalonia.Skia
 
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
+
+            var fontManager = new FontManagerImpl();
+
+            AvaloniaLocator.CurrentMutable
+                .Bind<IFontManagerImpl>().ToConstant(fontManager);
         }
 
         /// <summary>

+ 17 - 69
src/Skia/Avalonia.Skia/TypefaceCache.cs

@@ -1,7 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System.Collections.Generic;
+using System.Collections.Concurrent;
 using Avalonia.Media;
 using SkiaSharp;
 
@@ -12,88 +12,36 @@ namespace Avalonia.Skia
     /// </summary>
     internal static class TypefaceCache
     {
-        public static readonly string DefaultFamilyName = CreateDefaultFamilyName();
+        private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
+            new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
 
-        private static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> s_cache =
-            new Dictionary<string, Dictionary<FontKey, SKTypeface>>();
-
-        struct FontKey
+        public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
         {
-            public readonly SKFontStyleSlant Slant;
-            public readonly SKFontStyleWeight Weight;
-
-            public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
+            if (fontFamily.Key != null)
             {
-                Slant = slant;
-                Weight = weight;
+                return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
+                    .Get(fontFamily.Name, fontWeight, fontStyle);
             }
 
-            public override int GetHashCode()
-            {
-                int hash = 17;
-                hash = hash * 31 + (int)Slant;
-                hash = hash * 31 + (int)Weight;
-
-                return hash;
-            }
-
-            public override bool Equals(object other)
-            {
-                return other is FontKey ? Equals((FontKey)other) : false;
-            }
-
-            public bool Equals(FontKey other)
-            {
-                return Slant == other.Slant &&
-                       Weight == other.Weight;
-            }
-
-            // Equals and GetHashCode ommitted
-        }
-
-        private static string CreateDefaultFamilyName()
-        {
-            var defaultTypeface = SKTypeface.CreateDefault();
+            var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
 
-            return defaultTypeface.FamilyName;
-        }
+            var key = new FontKey(fontWeight, fontStyle);
 
-        private static SKTypeface GetTypeface(string name, FontKey key)
-        {
-            var familyKey = name;
-
-            if (!s_cache.TryGetValue(familyKey, out var entry))
+            if (typefaceCollection.TryGetValue(key, out var entry))
             {
-                s_cache[familyKey] = entry = new Dictionary<FontKey, SKTypeface>();
+                return entry;
             }
 
-            if (!entry.TryGetValue(key, out var typeface))
-            {
-                typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ??
-                           GetTypeface(DefaultFamilyName, key);
+            var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
+                                 SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
 
-                entry[key] = typeface;
-            }
+            var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
 
-            return typeface;
-        }
-
-        public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight)
-        {
-            var skStyle = SKFontStyleSlant.Upright;
+            entry = new TypefaceCollectionEntry(typeface, skTypeface);
 
-            switch (style)
-            {
-                case FontStyle.Italic:
-                    skStyle = SKFontStyleSlant.Italic;
-                    break;
-
-                case FontStyle.Oblique:
-                    skStyle = SKFontStyleSlant.Oblique;
-                    break;
-            }
+            typefaceCollection[key] = entry;
 
-            return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle));
+            return entry;
         }
     }
 }

+ 19 - 0
src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs

@@ -0,0 +1,19 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Media;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    internal class TypefaceCollectionEntry
+    {
+        public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface)
+        {
+            Typeface = typeface;
+            SKTypeface = skTypeface;
+        }
+        public Typeface Typeface { get; }
+        public SKTypeface SKTypeface { get; }
+    }
+}

+ 1 - 0
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -14,6 +14,7 @@
   </ItemGroup>
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="..\..\..\build\SharpDX.props" />
+  <Import Project="..\..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
   <Import Project="..\..\..\build\JetBrains.Annotations.props" />
 </Project>

+ 11 - 14
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using Avalonia.Controls;
@@ -27,6 +28,8 @@ namespace Avalonia.Direct2D1
 {
     public class Direct2D1Platform : IPlatformRenderInterface
     {
+        private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
+            new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
         private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
 
         public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1
 
         public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; }
 
-        public IEnumerable<string> InstalledFontNames
-        {
-            get
-            {
-                var cache = Direct2D1FontCollectionCache.s_installedFontCollection;
-                var length = cache.FontFamilyCount;
-                for (int i = 0; i < length; i++)
-                {
-                    var names = cache.GetFontFamily(i).FamilyNames;
-                    yield return names.GetString(0);
-                }
-            }
-        }
-
         private static readonly object s_initLock = new object();
         private static bool s_initialized = false;
 
@@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1
         {
             InitializeDirect2D();
             AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
+            AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
             SharpDX.Configuration.EnableReleaseOnFinalizer = true;
         }
 
@@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1
         public IFormattedTextImpl CreateFormattedText(
             string text,
             Typeface typeface,
+            double fontSize,
             TextAlignment textAlignment,
             TextWrapping wrapping,
             Size constraint,
@@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1
             return new FormattedTextImpl(
                 text,
                 typeface,
+                fontSize,
                 textAlignment,
                 wrapping,
                 constraint,
@@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1
         {
             return new WicBitmapImpl(format, data, size, dpi, stride);
         }
+
+        public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+        {
+            return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+        }
     }
 }

+ 24 - 25
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@@ -1,62 +1,61 @@
 using System.Collections.Concurrent;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = SharpDX.DirectWrite.FontStyle;
+using FontWeight = SharpDX.DirectWrite.FontWeight;
 
 namespace Avalonia.Direct2D1.Media
 {
     internal static class Direct2D1FontCollectionCache
     {
-        private static readonly ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection> s_cachedCollections;
-        internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection;
+        private static readonly ConcurrentDictionary<FontFamilyKey, FontCollection> s_cachedCollections;
+        internal static readonly FontCollection InstalledFontCollection;
 
         static Direct2D1FontCollectionCache()
         {
-            s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection>();
+            s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, FontCollection>();
 
-            s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
+            InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
         }
 
-        public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface)
+        public static Font GetFont(Typeface typeface)
         {
             var fontFamily = typeface.FontFamily;
             var fontCollection = GetOrAddFontCollection(fontFamily);
-            var fontFamilyName = FontFamily.Default.Name;
 
-            // Should this be cached?
             foreach (var familyName in fontFamily.FamilyNames)
             {
-                if (!fontCollection.FindFamilyName(familyName, out _))
+                if (fontCollection.FindFamilyName(familyName, out var index))
                 {
-                    continue;
+                    return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
+                        (FontWeight)typeface.Weight,
+                        FontStretch.Normal,
+                        (FontStyle)typeface.Style);
                 }
-
-                fontFamilyName = familyName;
-
-                break;
             }
 
-            return new SharpDX.DirectWrite.TextFormat(
-                Direct2D1Platform.DirectWriteFactory, 
-                fontFamilyName, 
-                fontCollection, 
-                (SharpDX.DirectWrite.FontWeight)typeface.Weight,
-                (SharpDX.DirectWrite.FontStyle)typeface.Style, 
-                SharpDX.DirectWrite.FontStretch.Normal, 
-                (float)typeface.FontSize);
+            InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
+
+            return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
+                (FontWeight)typeface.Weight,
+                FontStretch.Normal,
+                (FontStyle)typeface.Style);
         }
 
-        private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily)
+        private static FontCollection GetOrAddFontCollection(FontFamily fontFamily)
         {
-            return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
+            return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
         }
 
-        private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key)
+        private static FontCollection CreateFontCollection(FontFamilyKey key)
         {
             var assets = FontFamilyLoader.LoadFontAssets(key);
 
             var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
 
-            return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
+            return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
         }
     }
 }

+ 71 - 0
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@@ -0,0 +1,71 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = Avalonia.Media.FontStyle;
+using FontWeight = Avalonia.Media.FontWeight;
+
+namespace Avalonia.Direct2D1.Media
+{
+    internal class FontManagerImpl : IFontManagerImpl
+    {
+        public FontManagerImpl()
+        {
+            //ToDo: Implement a real lookup of the system's default font.
+            DefaultFontFamilyName = "segoe ui";
+        }
+
+        public string DefaultFontFamilyName { get; }
+
+        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        {
+            var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
+
+            var fontFamilies = new string[familyCount];
+
+            for (var i = 0; i < familyCount; i++)
+            {
+                fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
+            }
+
+            return fontFamilies;
+        }
+
+        public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+        {
+            //ToDo: Implement caching.
+            return new Typeface(fontFamily, fontWeight, fontStyle);
+        }
+
+        public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+            FontFamily fontFamily = null, CultureInfo culture = null)
+        {
+            var fontFamilyName = FontFamily.Default.Name;
+
+            var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
+
+            for (var i = 0; i < familyCount; i++)
+            {
+                var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
+                    .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
+                        (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
+
+                if (!font.HasCharacter(codepoint))
+                {
+                    continue;
+                }
+
+                fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
+
+                break;
+            }
+
+            return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+        }
+    }
+}

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