Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/master' into refactor/other-controls-itemssource

Max Katz 2 anni fa
parent
commit
0c0fa94d0e
100 ha cambiato i file con 2401 aggiunte e 1341 eliminazioni
  1. 1 1
      samples/ControlCatalog.Android/MainActivity.cs
  2. 4 0
      samples/ControlCatalog.Android/Resources/values-night/colors.xml
  3. 0 3
      samples/ControlCatalog/MainView.xaml
  4. 0 222
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  5. 0 68
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs
  6. 261 29
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  7. 37 0
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
  8. 14 0
      samples/VirtualizationDemo/App.axaml
  9. 20 0
      samples/VirtualizationDemo/App.axaml.cs
  10. 0 7
      samples/VirtualizationDemo/App.xaml
  11. 0 21
      samples/VirtualizationDemo/App.xaml.cs
  12. 190 0
      samples/VirtualizationDemo/Assets/chat.json
  13. 20 0
      samples/VirtualizationDemo/MainWindow.axaml
  14. 15 0
      samples/VirtualizationDemo/MainWindow.axaml.cs
  15. 0 64
      samples/VirtualizationDemo/MainWindow.xaml
  16. 0 22
      samples/VirtualizationDemo/MainWindow.xaml.cs
  17. 23 0
      samples/VirtualizationDemo/Models/Chat.cs
  18. 9 10
      samples/VirtualizationDemo/Program.cs
  19. 17 0
      samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs
  20. 21 0
      samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs
  21. 17 0
      samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs
  22. 0 26
      samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
  23. 7 157
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  24. 17 0
      samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs
  25. 95 0
      samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs
  26. 39 0
      samples/VirtualizationDemo/Views/ChatPageView.axaml
  27. 11 0
      samples/VirtualizationDemo/Views/ChatPageView.axaml.cs
  28. 18 0
      samples/VirtualizationDemo/Views/ExpanderPageView.axaml
  29. 13 0
      samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs
  30. 66 0
      samples/VirtualizationDemo/Views/PlaygroundPageView.axaml
  31. 44 0
      samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs
  32. 13 8
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  33. 0 4
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  34. 42 7
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  35. 3 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  36. 34 7
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  37. 6 0
      src/Avalonia.Base/AvaloniaProperty.cs
  38. 5 9
      src/Avalonia.Base/CombinedGeometry.cs
  39. 21 5
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  40. 1 1
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  41. 5 0
      src/Avalonia.Base/DirectPropertyBase.cs
  42. 30 23
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  43. 18 6
      src/Avalonia.Base/Layout/LayoutManager.cs
  44. 2 2
      src/Avalonia.Base/Media/DrawingContext.cs
  45. 45 11
      src/Avalonia.Base/Media/FontManager.cs
  46. 8 198
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  47. 259 0
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  48. 17 0
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  49. 14 52
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  50. 4 1
      src/Avalonia.Base/Media/GeometryGroup.cs
  51. 67 25
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  52. 16 2
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  53. 1 3
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  54. 2 2
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  55. 47 2
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  56. 35 19
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  57. 45 15
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  58. 68 17
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  59. 59 14
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  60. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  61. 15 0
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  62. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  63. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  64. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  65. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  66. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  67. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  68. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  69. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  70. 1 1
      src/Avalonia.Base/StyledElement.cs
  71. 5 0
      src/Avalonia.Base/StyledProperty.cs
  72. 4 4
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  73. 1 1
      src/Avalonia.Base/Visual.cs
  74. 0 0
      src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs
  75. 1 1
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  76. 5 1
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
  77. 33 7
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  78. 2 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  79. 1 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  80. 2 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  81. 2 0
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  82. 1 0
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml
  83. 2 0
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  84. 4 7
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  85. 2 2
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  86. 21 8
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  87. 13 3
      src/Avalonia.Controls/Flyouts/Flyout.cs
  88. 3 10
      src/Avalonia.Controls/ISelectable.cs
  89. 53 7
      src/Avalonia.Controls/ItemsControl.cs
  90. 2 1
      src/Avalonia.Controls/ListBoxItem.cs
  91. 1 1
      src/Avalonia.Controls/MenuItem.cs
  92. 7 1
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  93. 102 41
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  94. 8 8
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  95. 63 0
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  96. 0 1
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  97. 57 108
      src/Avalonia.Controls/Primitives/RangeBase.cs
  98. 74 5
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  99. 75 48
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  100. 6 3
      src/Avalonia.Controls/Primitives/Thumb.cs

+ 1 - 1
samples/ControlCatalog.Android/MainActivity.cs

@@ -5,7 +5,7 @@ using Avalonia.Android;
 
 namespace ControlCatalog.Android
 {
-    [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
+    [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
     public class MainActivity : AvaloniaMainActivity
     {
     }

+ 4 - 0
samples/ControlCatalog.Android/Resources/values-night/colors.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="splash_background">#212121</color>
+</resources>

+ 0 - 3
samples/ControlCatalog/MainView.xaml

@@ -147,9 +147,6 @@
       <TabItem Header="ScrollViewer">
         <pages:ScrollViewerPage />
       </TabItem>
-      <TabItem Header="ScrollViewer Snapping">
-        <pages:ScrollSnapPage />
-      </TabItem>
       <TabItem Header="Slider">
         <pages:SliderPage />
       </TabItem>

+ 0 - 222
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@@ -1,222 +0,0 @@
-<UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-             d:DesignHeight="800"
-             d:DesignWidth="400"
-             x:Class="ControlCatalog.Pages.ScrollSnapPage"
-             xmlns:pages="using:ControlCatalog.Pages"
-             x:DataType="pages:ScrollSnapPageViewModel">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock TextWrapping="Wrap"
-               Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
-
-    <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
-      <StackPanel Orientation="Horizontal"
-                  Spacing="4">
-        <StackPanel Orientation="Vertical"
-                    Spacing="4">
-          <TextBlock Text="Snap Point Type" />
-          <ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
-                    SelectedItem="{Binding SnapPointsType}" />
-        </StackPanel>
-
-        <StackPanel Orientation="Vertical"
-                    Spacing="4">
-          <TextBlock Text="Snap Point Alignment" />
-          <ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
-                    SelectedItem="{Binding SnapPointsAlignment}" />
-        </StackPanel>
-
-        <ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
-                      OffContent="No"
-                      OnContent="Yes"
-                      Content="Are Snap Points regular?" />
-      </StackPanel>
-      <TextBlock TextWrapping="Wrap"
-                 Grid.Row="1"
-                 Margin="0,10"
-                 Classes="h2">Vertical Snapping</TextBlock>
-
-      <Border
-        BorderBrush="Green"
-        BorderThickness="1"
-        Padding="0"
-        Grid.Row="2"
-        Margin="10, 5">
-        <ScrollViewer x:Name="VerticalSnapsScrollViewer"
-                      VerticalSnapPointsType="{Binding SnapPointsType}"
-                      VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
-                      HorizontalAlignment="Stretch"
-                      Height="350"
-                      HorizontalScrollBarVisibility="Disabled">
-          <StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
-                      Orientation="Vertical"
-                      HorizontalAlignment="Stretch">
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 1"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 2"/>
-            </Border>
-            <Border Padding="5, 20"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 3"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 4"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 5"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 6"/>
-            </Border>
-            <Border Padding="5,8"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 7"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 8"/>
-            </Border>
-            <Border Padding="5,4"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 9"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 20"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 11"/>
-            </Border>
-            <Border Padding="5, 30"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         Text="Child 12"/>
-            </Border>
-          </StackPanel>
-        </ScrollViewer>
-      </Border>
-      <TextBlock TextWrapping="Wrap"
-                 Grid.Row="3"
-                 Margin="0,10"
-                 Classes="h2">Horizontal Snapping</TextBlock>
-      <Border
-        BorderBrush="Green"
-        BorderThickness="1"
-        Padding="0"
-        Grid.Row="4"
-        Margin="10, 10">
-        <ScrollViewer x:Name="HorizontalSnapsScrollViewer"
-                      HorizontalSnapPointsType="{Binding SnapPointsType}"
-                      HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
-                      HorizontalAlignment="Stretch"
-                      Height="350"
-                      HorizontalScrollBarVisibility="Auto"
-                      VerticalScrollBarVisibility="Disabled">
-          <StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
-                      Orientation="Horizontal"
-                      HorizontalAlignment="Stretch">
-            <Border Padding="5, 30"
-                    Width="300"
-                    BorderBrush="Red"
-                    HorizontalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 1"/>
-            </Border>
-            <Border Padding="5, 30"
-                    Width="300"
-                    BorderBrush="Red"
-                    VerticalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 2"/>
-            </Border>
-            <Border Padding="5, 20"
-                    Width="300"
-                    BorderBrush="Red"
-                    VerticalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 3"/>
-            </Border>
-            <Border Padding="5, 30"
-                    Width="300"
-                    BorderBrush="Red"
-                    VerticalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 4"/>
-            </Border>
-            <Border Padding="5, 30"
-                    Width="300"
-                    BorderBrush="Red"
-                    VerticalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 5"/>
-            </Border>
-            <Border Padding="5, 30"
-                    Width="300"
-                    BorderBrush="Red"
-                    VerticalAlignment="Stretch"
-                    BorderThickness="1">
-              <TextBlock FontWeight="Bold"
-                         VerticalAlignment="Center"
-                         Text="Child 6"/>
-            </Border>
-            
-          </StackPanel>
-        </ScrollViewer>
-      </Border>
-    </Grid>
-  </StackPanel>
-</UserControl>

+ 0 - 68
samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs

@@ -1,68 +0,0 @@
-using System.Collections.Generic;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Markup.Xaml;
-using MiniMvvm;
-
-namespace ControlCatalog.Pages
-{
-    public class ScrollSnapPageViewModel : ViewModelBase
-    {
-        private SnapPointsType _snapPointsType;
-        private SnapPointsAlignment _snapPointsAlignment;
-        private bool _areSnapPointsRegular;
-
-        public ScrollSnapPageViewModel()
-        {
-
-            AvailableSnapPointsType = new List<SnapPointsType>()
-            {
-                SnapPointsType.None,
-                SnapPointsType.Mandatory,
-                SnapPointsType.MandatorySingle
-            };
-
-            AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
-            {
-                SnapPointsAlignment.Near,
-                SnapPointsAlignment.Center,
-                SnapPointsAlignment.Far,
-            };
-        }
-
-        public bool AreSnapPointsRegular
-        {
-            get => _areSnapPointsRegular;
-            set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value);
-        }
-
-        public SnapPointsType SnapPointsType
-        {
-            get => _snapPointsType;
-            set => this.RaiseAndSetIfChanged(ref _snapPointsType, value);
-        }
-
-        public SnapPointsAlignment SnapPointsAlignment
-        {
-            get => _snapPointsAlignment;
-            set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value);
-        }
-        public List<SnapPointsType> AvailableSnapPointsType { get; }
-        public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
-    }
-
-    public class ScrollSnapPage : UserControl
-    {
-        public ScrollSnapPage()
-        {
-            this.InitializeComponent();
-
-            DataContext = new ScrollSnapPageViewModel();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-    }
-}

+ 261 - 29
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@@ -3,35 +3,267 @@
              xmlns:pages="using:ControlCatalog.Pages"
              x:Class="ControlCatalog.Pages.ScrollViewerPage"
              x:DataType="pages:ScrollViewerPageViewModel">
-  <StackPanel Orientation="Vertical" Spacing="20">
-    <TextBlock TextWrapping="Wrap" Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
-
-    <Grid ColumnDefinitions="Auto, *">
-      <StackPanel Orientation="Vertical" Spacing="4">
-        <ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
-        <ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
-
-        <StackPanel Orientation="Vertical" Spacing="4">
-          <TextBlock Text="Horizontal Scroll" />
-          <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
-        </StackPanel>
-
-        <StackPanel Orientation="Vertical" Spacing="4">
-          <TextBlock Text="Vertical Scroll" />
-          <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
-        </StackPanel>
+  <TabControl>
+    <TabItem Header="ScrollViewer">
+      <StackPanel Orientation="Vertical"
+                  Spacing="20">
+        <TextBlock TextWrapping="Wrap"
+                   Classes="h2">Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling.</TextBlock>
+
+        <Grid ColumnDefinitions="Auto, *">
+          <StackPanel Orientation="Vertical"
+                      Spacing="4">
+            <ToggleSwitch IsChecked="{Binding AllowAutoHide}"
+                          Content="Allow auto hide" />
+            <ToggleSwitch IsChecked="{Binding EnableInertia}"
+                          Content="Enable Inertia" />
+
+            <StackPanel Orientation="Vertical"
+                        Spacing="4">
+              <TextBlock Text="Horizontal Scroll" />
+              <ComboBox ItemsSource="{Binding AvailableVisibility}"
+                        SelectedItem="{Binding HorizontalScrollVisibility}" />
+            </StackPanel>
+
+            <StackPanel Orientation="Vertical"
+                        Spacing="4">
+              <TextBlock Text="Vertical Scroll" />
+              <ComboBox ItemsSource="{Binding AvailableVisibility}"
+                        SelectedItem="{Binding VerticalScrollVisibility}" />
+            </StackPanel>
+          </StackPanel>
+
+          <ScrollViewer x:Name="ScrollViewer"
+                        Grid.Column="1"
+                        Width="400"
+                        Height="400"
+                        IsScrollInertiaEnabled="{Binding EnableInertia}"
+                        AllowAutoHide="{Binding AllowAutoHide}"
+                        HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
+                        VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
+            <Image Width="800"
+                   Height="800"
+                   Stretch="UniformToFill"
+                   Source="/Assets/delicate-arch-896885_640.jpg" />
+          </ScrollViewer>
+        </Grid>
       </StackPanel>
+    </TabItem>
+    <TabItem Header="Snapping">
+      <StackPanel Orientation="Vertical"
+                  Spacing="4">
+        <TextBlock TextWrapping="Wrap"
+                   Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
+
+        <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
+          <StackPanel Orientation="Horizontal"
+                      Spacing="4">
+            <StackPanel Orientation="Vertical"
+                        Spacing="4">
+              <TextBlock Text="Snap Point Type" />
+              <ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
+                        SelectedItem="{Binding SnapPointsType}" />
+            </StackPanel>
 
-      <ScrollViewer x:Name="ScrollViewer"
-                    Grid.Column="1"
-                    Width="400" Height="400"
-                    IsScrollInertiaEnabled="{Binding EnableInertia}"
-                    AllowAutoHide="{Binding AllowAutoHide}"
-                    HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
-                    VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
-        <Image Width="800" Height="800" Stretch="UniformToFill"
-               Source="/Assets/delicate-arch-896885_640.jpg" />
-      </ScrollViewer>
-    </Grid>
-  </StackPanel>
+            <StackPanel Orientation="Vertical"
+                        Spacing="4">
+              <TextBlock Text="Snap Point Alignment" />
+              <ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
+                        SelectedItem="{Binding SnapPointsAlignment}" />
+            </StackPanel>
+
+            <ToggleSwitch IsChecked="{Binding AreSnapPointsRegular}"
+                          OffContent="No"
+                          OnContent="Yes"
+                          Content="Are Snap Points regular?" />
+          </StackPanel>
+          <TextBlock TextWrapping="Wrap"
+                     Grid.Row="1"
+                     Margin="0,10"
+                     Classes="h2">Vertical Snapping</TextBlock>
+
+          <Border
+            BorderBrush="Green"
+            BorderThickness="1"
+            Padding="0"
+            Grid.Row="2"
+            Margin="10, 5">
+            <ScrollViewer x:Name="VerticalSnapsScrollViewer"
+                          VerticalSnapPointsType="{Binding SnapPointsType}"
+                          VerticalSnapPointsAlignment="{Binding SnapPointsAlignment}"
+                          HorizontalAlignment="Stretch"
+                          Height="350"
+                          HorizontalScrollBarVisibility="Disabled">
+              <StackPanel AreVerticalSnapPointsRegular="{Binding AreSnapPointsRegular}"
+                          Orientation="Vertical"
+                          HorizontalAlignment="Stretch">
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 1"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 2"/>
+                </Border>
+                <Border Padding="5, 20"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 3"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 4"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 5"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 6"/>
+                </Border>
+                <Border Padding="5,8"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 7"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 8"/>
+                </Border>
+                <Border Padding="5,4"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 9"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 20"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 11"/>
+                </Border>
+                <Border Padding="5, 30"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             Text="Child 12"/>
+                </Border>
+              </StackPanel>
+            </ScrollViewer>
+          </Border>
+          <TextBlock TextWrapping="Wrap"
+                     Grid.Row="3"
+                     Margin="0,10"
+                     Classes="h2">Horizontal Snapping</TextBlock>
+          <Border
+            BorderBrush="Green"
+            BorderThickness="1"
+            Padding="0"
+            Grid.Row="4"
+            Margin="10, 10">
+            <ScrollViewer x:Name="HorizontalSnapsScrollViewer"
+                          HorizontalSnapPointsType="{Binding SnapPointsType}"
+                          HorizontalSnapPointsAlignment="{Binding SnapPointsAlignment}"
+                          HorizontalAlignment="Stretch"
+                          Height="350"
+                          HorizontalScrollBarVisibility="Auto"
+                          VerticalScrollBarVisibility="Disabled">
+              <StackPanel AreHorizontalSnapPointsRegular="{Binding AreSnapPointsRegular}"
+                          Orientation="Horizontal"
+                          HorizontalAlignment="Stretch">
+                <Border Padding="5, 30"
+                        Width="300"
+                        BorderBrush="Red"
+                        HorizontalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 1"/>
+                </Border>
+                <Border Padding="5, 30"
+                        Width="300"
+                        BorderBrush="Red"
+                        VerticalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 2"/>
+                </Border>
+                <Border Padding="5, 20"
+                        Width="300"
+                        BorderBrush="Red"
+                        VerticalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 3"/>
+                </Border>
+                <Border Padding="5, 30"
+                        Width="300"
+                        BorderBrush="Red"
+                        VerticalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 4"/>
+                </Border>
+                <Border Padding="5, 30"
+                        Width="300"
+                        BorderBrush="Red"
+                        VerticalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 5"/>
+                </Border>
+                <Border Padding="5, 30"
+                        Width="300"
+                        BorderBrush="Red"
+                        VerticalAlignment="Stretch"
+                        BorderThickness="1">
+                  <TextBlock FontWeight="Bold"
+                             VerticalAlignment="Center"
+                             Text="Child 6"/>
+                </Border>
+
+              </StackPanel>
+            </ScrollViewer>
+          </Border>
+        </Grid>
+      </StackPanel>
+    </TabItem>
+  </TabControl>  
 </UserControl>

+ 37 - 0
samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs

@@ -12,6 +12,9 @@ namespace ControlCatalog.Pages
         private bool _enableInertia;
         private ScrollBarVisibility _horizontalScrollVisibility;
         private ScrollBarVisibility _verticalScrollVisibility;
+        private SnapPointsType _snapPointsType;
+        private SnapPointsAlignment _snapPointsAlignment;
+        private bool _areSnapPointsRegular;
 
         public ScrollViewerPageViewModel()
         {
@@ -23,6 +26,20 @@ namespace ControlCatalog.Pages
                 ScrollBarVisibility.Disabled,
             };
 
+            AvailableSnapPointsType = new List<SnapPointsType>()
+            {
+                SnapPointsType.None,
+                SnapPointsType.Mandatory,
+                SnapPointsType.MandatorySingle
+            };
+
+            AvailableSnapPointsAlignment = new List<SnapPointsAlignment>()
+            {
+                SnapPointsAlignment.Near,
+                SnapPointsAlignment.Center,
+                SnapPointsAlignment.Far,
+            };
+
             HorizontalScrollVisibility = ScrollBarVisibility.Auto;
             VerticalScrollVisibility = ScrollBarVisibility.Auto;
             AllowAutoHide = true;
@@ -54,6 +71,26 @@ namespace ControlCatalog.Pages
         }
 
         public List<ScrollBarVisibility> AvailableVisibility { get; }
+
+        public bool AreSnapPointsRegular
+        {
+            get => _areSnapPointsRegular;
+            set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value);
+        }
+
+        public SnapPointsType SnapPointsType
+        {
+            get => _snapPointsType;
+            set => this.RaiseAndSetIfChanged(ref _snapPointsType, value);
+        }
+
+        public SnapPointsAlignment SnapPointsAlignment
+        {
+            get => _snapPointsAlignment;
+            set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value);
+        }
+        public List<SnapPointsType> AvailableSnapPointsType { get; }
+        public List<SnapPointsAlignment> AvailableSnapPointsAlignment { get; }
     }
 
     public class ScrollViewerPage : UserControl

+ 14 - 0
samples/VirtualizationDemo/App.axaml

@@ -0,0 +1,14 @@
+<Application xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="VirtualizationDemo.App">
+  <Application.Styles>
+    <FluentTheme/>
+  </Application.Styles>
+  <Application.Resources>
+    <ResourceDictionary>
+      <ResourceDictionary.MergedDictionaries>
+        <ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
+      </ResourceDictionary.MergedDictionaries>
+    </ResourceDictionary>
+  </Application.Resources>
+</Application>

+ 20 - 0
samples/VirtualizationDemo/App.axaml.cs

@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace VirtualizationDemo;
+
+public partial class App : Application
+{
+    public override void Initialize()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            desktop.MainWindow = new MainWindow();
+        base.OnFrameworkInitializationCompleted();
+    }
+}

+ 0 - 7
samples/VirtualizationDemo/App.xaml

@@ -1,7 +0,0 @@
-<Application xmlns="https://github.com/avaloniaui"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             x:Class="VirtualizationDemo.App">
-  <Application.Styles>
-    <SimpleTheme />
-  </Application.Styles>
-</Application>

+ 0 - 21
samples/VirtualizationDemo/App.xaml.cs

@@ -1,21 +0,0 @@
-using Avalonia;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Markup.Xaml;
-
-namespace VirtualizationDemo
-{
-    public class App : Application
-    {
-        public override void Initialize()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-
-        public override void OnFrameworkInitializationCompleted()
-        {
-            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-                desktop.MainWindow = new MainWindow();
-            base.OnFrameworkInitializationCompleted();
-        }
-    }
-}

+ 190 - 0
samples/VirtualizationDemo/Assets/chat.json

@@ -0,0 +1,190 @@
+{
+  "chat": [
+    {
+      "sender": "Alice",
+      "message": "Hey Bob! How was your weekend?",
+      "timestamp": "2023-04-01T10:00:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?",
+      "timestamp": "2023-04-01T10:01:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.",
+      "timestamp": "2023-04-01T10:03:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "That sounds relaxing. What shows did you watch?",
+      "timestamp": "2023-04-01T10:05:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?",
+      "timestamp": "2023-04-01T10:07:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?",
+      "timestamp": "2023-04-01T10:10:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.",
+      "timestamp": "2023-04-01T10:12:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.",
+      "timestamp": "2023-04-01T10:15:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?",
+      "timestamp": "2023-04-01T10:20:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's been pretty busy, but I'm managing. How about you?",
+      "timestamp": "2023-04-01T10:22:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?",
+      "timestamp": "2023-04-01T10:25:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Absolutely. Hey, have you heard about the new project we're starting next week?",
+      "timestamp": "2023-04-01T10:30:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "No, I haven't. What's it about?",
+      "timestamp": "2023-04-01T10:32:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?",
+      "timestamp": "2023-04-01T10:35:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Sounds good to me. I could use a break right about now.",
+      "timestamp": "2023-04-01T10:40:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Me too. So, have you tried the new caf� down the street yet?",
+      "timestamp": "2023-04-01T10:45:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "No, I haven't. Is it any good?",
+      "timestamp": "2023-04-01T10:47:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "It's really good! They have the best croissants I've ever tasted.",
+      "timestamp": "2023-04-01T10:50:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?",
+      "timestamp": "2023-04-01T10:52:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "I'm not sure, but I think they do. You should ask them the next time you go there.",
+      "timestamp": "2023-04-01T10:55:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Thanks for the suggestion. I'm always looking for good vegan options around here.",
+      "timestamp": "2023-04-01T11:00:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "No problem. So, have you made any plans for the weekend yet?",
+      "timestamp": "2023-04-01T11:05:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Not yet. I was thinking of maybe going for a hike or something. What about you?",
+      "timestamp": "2023-04-01T11:07:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "I haven't made any plans either. Maybe we could do something together?",
+      "timestamp": "2023-04-01T11:10:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "That sounds like a great idea! Let's plan on it.",
+      "timestamp": "2023-04-01T11:12:00"
+    },
+    {
+      "sender": "Bob",
+      "message": "Awesome. I'll check out some hiking trails and let you know which ones look good.",
+      "timestamp": "2023-04-01T11:15:00"
+    },
+    {
+      "sender": "Alice",
+      "message": "Sounds good. I can't wait!",
+      "timestamp": "2023-04-01T11:20:00"
+    },
+    {
+      "sender": "John",
+      "message": "Hey Lisa, how was your day?",
+      "timestamp": "2023-04-01T18:00:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "It was good, thanks for asking. How about you?",
+      "timestamp": "2023-04-01T18:05:00"
+    },
+    {
+      "sender": "John",
+      "message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.",
+      "timestamp": "2023-04-01T18:10:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.",
+      "timestamp": "2023-04-01T18:15:00"
+    },
+    {
+      "sender": "John",
+      "message": "That sucks. Are you feeling stressed out?",
+      "timestamp": "2023-04-01T18:20:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.",
+      "timestamp": "2023-04-01T18:25:00"
+    },
+    {
+      "sender": "John",
+      "message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?",
+      "timestamp": "2023-04-01T18:30:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "I haven't, but I've been thinking about it. Do you have any suggestions?",
+      "timestamp": "2023-04-01T18:35:00"
+    },
+    {
+      "sender": "John",
+      "message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.",
+      "timestamp": "2023-04-01T18:40:00"
+    },
+    {
+      "sender": "Lisa",
+      "message": "That would be awesome, thanks so much!",
+      "timestamp": "2023-04-01T18:45:00"
+    }
+  ]
+}
+

+ 20 - 0
samples/VirtualizationDemo/MainWindow.axaml

@@ -0,0 +1,20 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:ControlSamples"
+        xmlns:vm="using:VirtualizationDemo.ViewModels"
+        xmlns:views="using:VirtualizationDemo.Views"
+        x:Class="VirtualizationDemo.MainWindow"
+        Title="AvaloniaUI Virtualization Demo"
+        x:DataType="vm:MainWindowViewModel">
+  <controls:HamburgerMenu>
+    <TabItem Header="Playground" ScrollViewer.VerticalScrollBarVisibility="Disabled">
+      <views:PlaygroundPageView DataContext="{Binding Playground}"/>
+    </TabItem>
+    <TabItem Header="Chat" ScrollViewer.VerticalScrollBarVisibility="Disabled">
+      <views:ChatPageView DataContext="{Binding Chat}"/>
+    </TabItem>
+    <TabItem Header="Expanders" ScrollViewer.VerticalScrollBarVisibility="Disabled">
+      <views:ExpanderPageView DataContext="{Binding Expanders}"/>
+    </TabItem>
+  </controls:HamburgerMenu>
+</Window>

+ 15 - 0
samples/VirtualizationDemo/MainWindow.axaml.cs

@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.Controls;
+using VirtualizationDemo.ViewModels;
+
+namespace VirtualizationDemo;
+
+public partial class MainWindow : Window
+{
+    public MainWindow()
+    {
+        InitializeComponent();
+        this.AttachDevTools();
+        DataContext = new MainWindowViewModel();
+    }
+}

+ 0 - 64
samples/VirtualizationDemo/MainWindow.xaml

@@ -1,64 +0,0 @@
-<Window xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:viewModels="using:VirtualizationDemo.ViewModels"
-        x:Class="VirtualizationDemo.MainWindow"
-        Title="AvaloniaUI Virtualization Test"
-        Width="800"
-        Height="600"
-        x:DataType="viewModels:MainWindowViewModel">
-    <DockPanel LastChildFill="True" Margin="16">
-        <StackPanel DockPanel.Dock="Right" 
-                    Margin="16 0 0 0" 
-                    Width="150"
-                    Spacing="4">
-            <ComboBox ItemsSource="{Binding Orientations}"
-                      SelectedItem="{Binding Orientation}"/>
-            <TextBox Watermark="Item Count"
-                     UseFloatingWatermark="True"
-                     Text="{Binding ItemCount}"/>
-            <TextBox Watermark="Extent"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/>
-            <TextBox Watermark="Offset"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/>
-            <TextBox Watermark="Viewport"
-                     UseFloatingWatermark="True"
-                     Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
-            <TextBlock>Horiz. ScrollBar</TextBlock>
-            <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
-                      SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
-            <TextBlock>Vert. ScrollBar</TextBlock>
-            <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
-                      SelectedItem="{Binding VerticalScrollBarVisibility}"/>
-            <TextBox Watermark="Item to Create"
-                     UseFloatingWatermark="True"
-                     Text="{Binding NewItemString}"/>
-            <Button Command="{Binding AddItemCommand}">Add Item</Button>
-            <Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
-            <Button Command="{Binding RecreateCommand}">Recreate</Button>
-            <Button Command="{Binding SelectFirstCommand}">Select First</Button>
-            <Button Command="{Binding SelectLastCommand}">Select Last</Button>
-            <Button Command="{Binding RandomizeSize}">Randomize Size</Button>
-            <Button Command="{Binding ResetSize}">Reset Size</Button>
-        </StackPanel>
-
-        <ListBox Name="listBox" 
-                 ItemsSource="{Binding Items}" 
-                 Selection="{Binding Selection}"
-                 SelectionMode="Multiple"
-                 ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"
-                 ScrollViewer.VerticalScrollBarVisibility="{Binding VerticalScrollBarVisibility, Mode=TwoWay}">
-            <ListBox.ItemsPanel>
-                <ItemsPanelTemplate>
-                    <VirtualizingStackPanel Orientation="{Binding Orientation}"/>
-                </ItemsPanelTemplate>
-            </ListBox.ItemsPanel>
-            <ListBox.ItemTemplate>
-                <DataTemplate>
-                    <TextBlock Text="{Binding Header}" Height="{Binding Height}" TextWrapping="Wrap"/>
-                </DataTemplate>
-            </ListBox.ItemTemplate>
-        </ListBox>
-    </DockPanel>
-</Window>

+ 0 - 22
samples/VirtualizationDemo/MainWindow.xaml.cs

@@ -1,22 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using VirtualizationDemo.ViewModels;
-
-namespace VirtualizationDemo
-{
-    public class MainWindow : Window
-    {
-        public MainWindow()
-        {
-            this.InitializeComponent();
-            this.AttachDevTools();
-            DataContext = new MainWindowViewModel();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-    }
-}

+ 23 - 0
samples/VirtualizationDemo/Models/Chat.cs

@@ -0,0 +1,23 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace VirtualizationDemo.Models;
+
+public class ChatFile
+{
+    public ChatMessage[]? Chat { get; set; }
+
+    public static ChatFile Load(string path)
+    {
+        var options = new JsonSerializerOptions
+        {
+            PropertyNameCaseInsensitive = true
+        };
+
+        using var s = File.OpenRead(path);
+        return JsonSerializer.Deserialize<ChatFile>(s, options)!;
+    }
+}
+
+public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp);

+ 9 - 10
samples/VirtualizationDemo/Program.cs

@@ -1,15 +1,14 @@
 using Avalonia;
 
-namespace VirtualizationDemo
+namespace VirtualizationDemo;
+
+class Program
 {
-    class Program
-    {
-        public static AppBuilder BuildAvaloniaApp()
-            => AppBuilder.Configure<App>()
-                .UsePlatformDetect()
-                .LogToTrace();
+    public static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure<App>()
+            .UsePlatformDetect()
+            .LogToTrace();
 
-        public static int Main(string[] args)
-            => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
-    }
+    public static int Main(string[] args)
+        => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
 }

+ 17 - 0
samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using VirtualizationDemo.Models;
+
+namespace VirtualizationDemo.ViewModels;
+
+public class ChatPageViewModel
+{
+    public ChatPageViewModel()
+    {
+        var chat = ChatFile.Load(Path.Combine("Assets", "chat.json"));
+        Messages = new(chat.Chat ?? Array.Empty<ChatMessage>());
+    }
+
+    public ObservableCollection<ChatMessage> Messages { get; }
+}

+ 21 - 0
samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs

@@ -0,0 +1,21 @@
+using MiniMvvm;
+
+namespace VirtualizationDemo.ViewModels;
+
+public class ExpanderItemViewModel : ViewModelBase
+{
+    private string? _header;
+    private bool _isExpanded;
+
+    public string? Header 
+    { 
+        get => _header;
+        set => RaiseAndSetIfChanged(ref _header, value);
+    }
+
+    public bool IsExpanded
+    {
+        get => _isExpanded;
+        set => RaiseAndSetIfChanged(ref _isExpanded, value);
+    }
+}

+ 17 - 0
samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs

@@ -0,0 +1,17 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace VirtualizationDemo.ViewModels;
+
+internal class ExpanderPageViewModel
+{
+    public ExpanderPageViewModel()
+    {
+        Items = new(Enumerable.Range(0, 100).Select(x => new  ExpanderItemViewModel
+        {
+            Header = $"Item {x}",
+        }));
+    }
+
+    public ObservableCollection<ExpanderItemViewModel> Items { get; set; }
+}

+ 0 - 26
samples/VirtualizationDemo/ViewModels/ItemViewModel.cs

@@ -1,26 +0,0 @@
-using System;
-using MiniMvvm;
-
-namespace VirtualizationDemo.ViewModels
-{
-    internal class ItemViewModel : ViewModelBase
-    {
-        private string _prefix;
-        private int _index;
-        private double _height = double.NaN;
-
-        public ItemViewModel(int index, string prefix = "Item")
-        {
-            _prefix = prefix;
-            _index = index;
-        }
-
-        public string Header => $"{_prefix} {_index}";
-
-        public double Height
-        {
-            get => _height;
-            set => this.RaiseAndSetIfChanged(ref _height, value);
-        }
-    }
-}

+ 7 - 157
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@@ -1,160 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive;
-using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
-using Avalonia.Layout;
-using Avalonia.Controls.Selection;
-using MiniMvvm;
+using MiniMvvm;
 
-namespace VirtualizationDemo.ViewModels
-{
-    internal class MainWindowViewModel : ViewModelBase
-    {
-        private int _itemCount = 200;
-        private string _newItemString = "New Item";
-        private int _newItemIndex;
-        private AvaloniaList<ItemViewModel> _items;
-        private string _prefix = "Item";
-        private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto;
-        private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto;
-        private Orientation _orientation = Orientation.Vertical;
-
-        public MainWindowViewModel()
-        {
-            this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
-            RecreateCommand = MiniCommand.Create(() => Recreate());
-
-            AddItemCommand = MiniCommand.Create(() => AddItem());
-
-            RemoveItemCommand = MiniCommand.Create(() => Remove());
-
-            SelectFirstCommand = MiniCommand.Create(() => SelectItem(0));
-
-            SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1));
-        }
-
-        public string NewItemString
-        {
-            get { return _newItemString; }
-            set { this.RaiseAndSetIfChanged(ref _newItemString, value); }
-        }
-
-        public int ItemCount
-        {
-            get { return _itemCount; }
-            set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
-        }
-
-        public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
-
-        public AvaloniaList<ItemViewModel> Items
-        {
-            get { return _items; }
-            private set { this.RaiseAndSetIfChanged(ref _items, value); }
-        }
-
-        public Orientation Orientation
-        {
-            get { return _orientation; }
-            set { this.RaiseAndSetIfChanged(ref _orientation, value); }
-        }
-
-        public IEnumerable<Orientation> Orientations =>
-            Enum.GetValues(typeof(Orientation)).Cast<Orientation>();
-
-        public ScrollBarVisibility HorizontalScrollBarVisibility
-        {
-            get { return _horizontalScrollBarVisibility; }
-            set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); }
-        }
+namespace VirtualizationDemo.ViewModels;
 
-        public ScrollBarVisibility VerticalScrollBarVisibility
-        {
-            get { return _verticalScrollBarVisibility; }
-            set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); }
-        }
-
-        public IEnumerable<ScrollBarVisibility> ScrollBarVisibilities =>
-            Enum.GetValues(typeof(ScrollBarVisibility)).Cast<ScrollBarVisibility>();
-
-        public MiniCommand AddItemCommand { get; private set; }
-        public MiniCommand RecreateCommand { get; private set; }
-        public MiniCommand RemoveItemCommand { get; private set; }
-        public MiniCommand SelectFirstCommand { get; private set; }
-        public MiniCommand SelectLastCommand { get; private set; }
-
-        public void RandomizeSize()
-        {
-            var random = new Random();
-
-            foreach (var i in Items)
-            {
-                i.Height = random.Next(240) + 10;
-            }
-        }
-
-        public void ResetSize()
-        {
-            foreach (var i in Items)
-            {
-                i.Height = double.NaN;
-            }
-        }
-
-        private void ResizeItems(int count)
-        {
-            if (Items == null)
-            {
-                var items = Enumerable.Range(0, count)
-                    .Select(x => new ItemViewModel(x));
-                Items = new AvaloniaList<ItemViewModel>(items);
-            }
-            else if (count > Items.Count)
-            {
-                var items = Enumerable.Range(Items.Count, count - Items.Count)
-                    .Select(x => new ItemViewModel(x));
-                Items.AddRange(items);
-            }
-            else if (count < Items.Count)
-            {
-                Items.RemoveRange(count, Items.Count - count);
-            }
-        }
-
-        private void AddItem()
-        {
-            var index = Items.Count;
-
-            if (Selection.SelectedItems.Count > 0)
-            {
-                index = Selection.SelectedIndex;
-            }
-
-            Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
-        }
-
-        private void Remove()
-        {
-            if (Selection.SelectedItems.Count > 0)
-            {
-                Items.RemoveAll(Selection.SelectedItems.ToList());
-            }
-        }
-
-        private void Recreate()
-        {
-            _prefix = _prefix == "Item" ? "Recreated" : "Item";
-            var items = Enumerable.Range(0, _itemCount)
-                .Select(x => new ItemViewModel(x, _prefix));
-            Items = new AvaloniaList<ItemViewModel>(items);
-        }
-
-        private void SelectItem(int index)
-        {
-            Selection.SelectedIndex = index;
-        }
-    }
+internal class MainWindowViewModel : ViewModelBase
+{
+    public PlaygroundPageViewModel Playground { get; } = new();
+    public ChatPageViewModel Chat { get; } = new();
+    public ExpanderPageViewModel Expanders { get; } = new();
 }

+ 17 - 0
samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs

@@ -0,0 +1,17 @@
+using MiniMvvm;
+
+namespace VirtualizationDemo.ViewModels;
+
+public class PlaygroundItemViewModel : ViewModelBase
+{
+    private string? _header;
+
+    public PlaygroundItemViewModel(int index) => Header = $"Item {index}";
+    public PlaygroundItemViewModel(string? header) => Header = header;
+
+    public string? Header
+    {
+        get => _header;
+        set => RaiseAndSetIfChanged(ref _header, value);
+    }
+}

+ 95 - 0
samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs

@@ -0,0 +1,95 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Selection;
+using MiniMvvm;
+
+namespace VirtualizationDemo.ViewModels;
+
+public class PlaygroundPageViewModel : ViewModelBase
+{
+    private SelectionMode _selectionMode = SelectionMode.Multiple;
+    private int _scrollToIndex = 500;
+    private string? _newItemHeader = "New Item 1";
+
+    public PlaygroundPageViewModel()
+    {
+        Items = new(Enumerable.Range(0, 1000).Select(x => new PlaygroundItemViewModel(x)));
+        Selection = new();
+    }
+
+    public ObservableCollection<PlaygroundItemViewModel> Items { get; }
+
+    public bool Multiple
+    {
+        get => _selectionMode.HasAnyFlag(SelectionMode.Multiple);
+        set => SetSelectionMode(SelectionMode.Multiple, value);
+    }
+
+    public bool Toggle
+    {
+        get => _selectionMode.HasAnyFlag(SelectionMode.Toggle);
+        set => SetSelectionMode(SelectionMode.Toggle, value);
+    }
+
+    public bool AlwaysSelected
+    {
+        get => _selectionMode.HasAnyFlag(SelectionMode.AlwaysSelected);
+        set => SetSelectionMode(SelectionMode.AlwaysSelected, value);
+    }
+
+    public SelectionModel<PlaygroundItemViewModel> Selection { get; }
+    
+    public SelectionMode SelectionMode
+    {
+        get => _selectionMode;
+        set => RaiseAndSetIfChanged(ref _selectionMode, value);
+    }
+
+    public int ScrollToIndex
+    {
+        get => _scrollToIndex;
+        set => RaiseAndSetIfChanged(ref _scrollToIndex, value);
+    }
+
+    public string? NewItemHeader
+    {
+        get => _newItemHeader;
+        set => RaiseAndSetIfChanged(ref _newItemHeader, value);
+    }
+
+    public void ExecuteScrollToIndex()
+    {
+        Selection.Select(ScrollToIndex);
+    }
+
+    public void RandomizeScrollToIndex()
+    {
+        var rnd = new Random();
+        ScrollToIndex = rnd.Next(Items.Count);
+    }
+
+    public void AddAtSelectedIndex()
+    {
+        if (Selection.SelectedIndex == -1)
+            return;
+        Items.Insert(Selection.SelectedIndex, new(NewItemHeader));
+    }
+
+    public void DeleteSelectedItem()
+    {
+        var count = Selection.Count;
+        for (var i = count - 1; i >= 0; i--)
+            Items.RemoveAt(Selection.SelectedIndexes[i]);
+    }
+
+    private void SetSelectionMode(SelectionMode mode, bool value)
+    {
+        if (value)
+            SelectionMode |= mode;
+        else
+            SelectionMode &= ~mode;
+    }
+}

+ 39 - 0
samples/VirtualizationDemo/Views/ChatPageView.axaml

@@ -0,0 +1,39 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="using:VirtualizationDemo.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="VirtualizationDemo.Views.ChatPageView"
+             x:DataType="vm:ChatPageViewModel">
+  <ListBox ItemsSource="{Binding Messages}">
+    <ListBox.ItemContainerTheme>
+      <ControlTheme TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
+        <Setter Property="Padding" Value="8"/>
+      </ControlTheme>
+    </ListBox.ItemContainerTheme>
+    <ListBox.ItemTemplate>
+      <DataTemplate>
+        <Border CornerRadius="8" 
+                Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
+                TextElement.Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}"
+                Padding="6"
+                HorizontalAlignment="Left"
+                MaxWidth="280">
+          <DockPanel>
+            <TextBlock DockPanel.Dock="Top"
+                       Text="{Binding Sender}"
+                       FontWeight="Bold"/>
+            <TextBlock DockPanel.Dock="Bottom" 
+                       Text="{Binding Timestamp}"
+                       FontSize="10"
+                       Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
+                       TextAlignment="Right"
+                       Margin="0 4 0 0"/>
+            <TextBlock Text="{Binding Message}" TextWrapping="Wrap"/>
+          </DockPanel>
+        </Border>
+      </DataTemplate>
+    </ListBox.ItemTemplate>
+  </ListBox>
+</UserControl>

+ 11 - 0
samples/VirtualizationDemo/Views/ChatPageView.axaml.cs

@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace VirtualizationDemo.Views;
+
+public partial class ChatPageView : UserControl
+{
+    public ChatPageView()
+    {
+        InitializeComponent();
+    }
+}

+ 18 - 0
samples/VirtualizationDemo/Views/ExpanderPageView.axaml

@@ -0,0 +1,18 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="using:VirtualizationDemo.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="VirtualizationDemo.Views.ExpanderPageView"
+             x:DataType="vm:ExpanderPageViewModel">
+  <ListBox ItemsSource="{Binding Items}">
+    <ListBox.ItemTemplate>
+      <DataTemplate>
+        <Expander Header="{Binding Header}" IsExpanded="{Binding IsExpanded}">
+          <Border Width="200" Height="300"/>
+        </Expander>
+      </DataTemplate>
+    </ListBox.ItemTemplate>
+  </ListBox>
+</UserControl>

+ 13 - 0
samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs

@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace VirtualizationDemo.Views;
+
+public partial class ExpanderPageView : UserControl
+{
+    public ExpanderPageView()
+    {
+        InitializeComponent();
+    }
+}

+ 66 - 0
samples/VirtualizationDemo/Views/PlaygroundPageView.axaml

@@ -0,0 +1,66 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:vm="using:VirtualizationDemo.ViewModels"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="VirtualizationDemo.Views.PlaygroundPageView"
+             x:DataType="vm:PlaygroundPageViewModel">
+  <DockPanel>
+    <StackPanel DockPanel.Dock="Right" Margin="8 0" Width="200">
+      <DropDownButton Content="Selection" HorizontalAlignment="Stretch">
+        <Button.Flyout>
+          <Flyout>
+            <StackPanel>
+              <CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
+              <CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
+              <CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
+              <CheckBox IsChecked="{Binding #list.AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
+              <CheckBox IsChecked="{Binding #list.WrapSelection}">WrapSelection</CheckBox>
+            </StackPanel>
+          </Flyout>
+        </Button.Flyout>
+      </DropDownButton>
+      
+      <Label>_Select Item</Label>
+      <DockPanel>
+        <TextBox x:Name="scrollToIndex" Text="{Binding ScrollToIndex}">
+          <TextBox.InnerRightContent>
+            <StackPanel Orientation="Horizontal">
+              <Button DockPanel.Dock="Right"
+                      Command="{Binding RandomizeScrollToIndex}"
+                      ToolTip.Tip="Randomize">
+                &#x27F3;
+              </Button>
+              <Button DockPanel.Dock="Right"
+                      Command="{Binding ExecuteScrollToIndex}"
+                      ToolTip.Tip="Execute">
+                &#11152;
+              </Button>
+            </StackPanel>
+          </TextBox.InnerRightContent>
+        </TextBox>
+      </DockPanel>
+
+      <Label>New Item</Label>
+      <TextBox Text="{Binding NewItemHeader}">
+        <TextBox.InnerRightContent>
+          <Button Command="{Binding AddAtSelectedIndex}"
+                  ToolTip.Tip="Add at Selected Index">&#x2B;</Button>
+        </TextBox.InnerRightContent>
+      </TextBox>
+
+      <Button Command="{Binding DeleteSelectedItem}" Margin="0 8 0 0">
+        Delete Selected
+      </Button>
+    </StackPanel>
+    
+    <TextBlock Name="itemCount" DockPanel.Dock="Bottom"/>
+    
+    <ListBox Name="list"
+             ItemsSource="{Binding Items}"
+             DisplayMemberBinding="{Binding Header}"
+             Selection="{Binding Selection}"
+             SelectionMode="{Binding SelectionMode}"/>
+  </DockPanel>
+</UserControl>

+ 44 - 0
samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using System.Threading;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+
+namespace VirtualizationDemo.Views;
+
+public partial class PlaygroundPageView : UserControl
+{
+    private DispatcherTimer _timer;
+
+    public PlaygroundPageView()
+    {
+        InitializeComponent();
+        
+        _timer = new DispatcherTimer
+        {
+            Interval = TimeSpan.FromMilliseconds(500),
+        };
+        
+        _timer.Tick += TimerTick;
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        _timer.Start();
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        _timer.Stop();
+    }
+
+    private void TimerTick(object? sender, EventArgs e)
+    {
+        var message = $"Realized {list.GetRealizedContainers().Count()} of {list.ItemsPanelRoot?.Children.Count}";
+        itemCount.Text = message;
+    }
+}

+ 13 - 8
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -1,19 +1,24 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <OutputType>Exe</OutputType>
+    <OutputType>WinExe</OutputType>
     <TargetFramework>net6.0</TargetFramework>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
   </PropertyGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
-    <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
-    <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
+    <ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
+  </ItemGroup>
+ <ItemGroup>
+    <None Update="Assets\chat.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
-  <Import Project="..\..\build\SampleApp.props" />
-  <Import Project="..\..\build\EmbedXaml.props" />
-  <Import Project="..\..\build\Rx.props" />
-  <Import Condition="'$(TargetFramework)'=='net461'" Project="..\..\build\NetFX.props" />
-  <Import Project="..\..\build\ReferenceCoreLibraries.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
+  <Import Project="..\..\build\SourceGenerators.props" />
+  <Import Project="..\..\build\NullableEnable.props" />
 </Project>

+ 0 - 4
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@@ -32,10 +32,6 @@ namespace Avalonia.Android
             {
                 lifetime.View = View;
             }
-
-            Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
-            Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
-
             base.OnCreate(savedInstanceState);
 
             SetContentView(View);

+ 42 - 7
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@@ -2,11 +2,10 @@
 using System.Collections.Generic;
 using Android.OS;
 using Android.Views;
-using AndroidX.AppCompat.App;
 using AndroidX.Core.View;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls.Platform;
-using static Avalonia.Controls.Platform.IInsetsManager;
+using Avalonia.Media;
 
 namespace Avalonia.Android.Platform
 {
@@ -20,6 +19,7 @@ namespace Avalonia.Android.Platform
         private bool? _systemUiVisibility;
         private SystemBarTheme? _statusBarTheme;
         private bool? _isDefaultSystemBarLightTheme;
+        private Color? _systemBarColor;
 
         public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
 
@@ -36,6 +36,16 @@ namespace Avalonia.Android.Platform
                 }
 
                 WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
+
+                if(value)
+                {
+                    _activity.Window.AddFlags(WindowManagerFlags.TranslucentStatus);
+                    _activity.Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
+                }
+                else
+                {
+                    SystemBarColor = _systemBarColor;
+                }
             }
         }
 
@@ -71,7 +81,7 @@ namespace Avalonia.Android.Platform
                     var renderScaling = _topLevel.RenderScaling;
 
                     var inset = insets.GetInsets(
-                        (DisplayEdgeToEdge ?
+                        (_displayEdgeToEdge ?
                             WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
                             WindowInsetsCompat.Type.DisplayCutout() :
                             0) | WindowInsetsCompat.Type.Ime());
@@ -81,8 +91,8 @@ namespace Avalonia.Android.Platform
                     return new Thickness(inset.Left / renderScaling,
                         inset.Top / renderScaling,
                         inset.Right / renderScaling,
-                        (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
-                            imeInset.Bottom - navBarInset.Bottom :
+                        (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
+                            imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
                             inset.Bottom) / renderScaling);
                 }
 
@@ -93,6 +103,7 @@ namespace Avalonia.Android.Platform
         public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
         {
             NotifySafeAreaChanged(SafeAreaPadding);
+            insets = ViewCompat.OnApplyWindowInsets(v, insets);
             return insets;
         }
 
@@ -146,8 +157,6 @@ namespace Avalonia.Android.Platform
 
                 compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
                 compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light;
-
-                AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
             }
         }
 
@@ -190,10 +199,36 @@ namespace Avalonia.Android.Platform
             }
         }
 
+        public Color? SystemBarColor
+        {
+            get => _systemBarColor; 
+            set
+            {
+                _systemBarColor = value;
+
+                if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null)
+                {
+                    _activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus);
+                    _activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation);
+                    _activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
+
+                    var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B);
+                    _activity.Window.SetStatusBarColor(androidColor);
+
+                    if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
+                    {
+                        // As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions
+                        _activity.Window.SetNavigationBarColor(androidColor);
+                    }
+                }
+            }
+        }
+
         internal void ApplyStatusBarState()
         {
             IsSystemBarVisible = _systemUiVisibility;
             SystemBarTheme = _statusBarTheme;
+            SystemBarColor = _systemBarColor;
         }
 
         private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback

+ 3 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -9,6 +9,7 @@ using Android.Runtime;
 using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
+using AndroidX.AppCompat.App;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific.Helpers;
 using Avalonia.Android.Platform.Storage;
@@ -286,6 +287,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                     _ => null,
                 };
             }
+
+            AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
         }
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);

+ 34 - 7
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -1,6 +1,6 @@
 using System;
-using Avalonia.Reactive;
 using Avalonia.Data;
+using Avalonia.Reactive;
 
 namespace Avalonia
 {
@@ -34,8 +34,8 @@ namespace Avalonia
         /// </remarks>
         public static IObservable<object?> GetObservable(this AvaloniaObject o, AvaloniaProperty property)
         {
-            return new AvaloniaPropertyObservable<object?>(
-                o ?? throw new ArgumentNullException(nameof(o)), 
+            return new AvaloniaPropertyObservable<object?, object?>(
+                o ?? throw new ArgumentNullException(nameof(o)),
                 property ?? throw new ArgumentNullException(nameof(property)));
         }
 
@@ -54,11 +54,23 @@ namespace Avalonia
         /// </remarks>
         public static IObservable<T> GetObservable<T>(this AvaloniaObject o, AvaloniaProperty<T> property)
         {
-            return new AvaloniaPropertyObservable<T>(
+            return new AvaloniaPropertyObservable<T, T>(
                 o ?? throw new ArgumentNullException(nameof(o)),
                 property ?? throw new ArgumentNullException(nameof(property)));
         }
 
+        /// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
+        /// <param name="o"/>
+        /// <param name="property"/>
+        /// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
+        public static IObservable<TResult> GetObservable<TSource, TResult>(this AvaloniaObject o, AvaloniaProperty<TSource> property, Func<TSource, TResult> converter)
+        {
+            return new AvaloniaPropertyObservable<TSource, TResult>(
+                o ?? throw new ArgumentNullException(nameof(o)),
+                property ?? throw new ArgumentNullException(nameof(property)),
+                converter ?? throw new ArgumentNullException(nameof(converter)));
+        }
+
         /// <summary>
         /// Gets an observable for an <see cref="AvaloniaProperty"/>.
         /// </summary>
@@ -75,7 +87,7 @@ namespace Avalonia
             this AvaloniaObject o,
             AvaloniaProperty property)
         {
-            return new AvaloniaPropertyBindingObservable<object?>(
+            return new AvaloniaPropertyBindingObservable<object?, object?>(
                 o ?? throw new ArgumentNullException(nameof(o)),
                 property ?? throw new ArgumentNullException(nameof(property)));
         }
@@ -97,12 +109,27 @@ namespace Avalonia
             this AvaloniaObject o,
             AvaloniaProperty<T> property)
         {
-            return new AvaloniaPropertyBindingObservable<T>(
+            return new AvaloniaPropertyBindingObservable<T, T>(
                 o ?? throw new ArgumentNullException(nameof(o)),
                 property ?? throw new ArgumentNullException(nameof(property)));
 
         }
 
+        /// <inheritdoc cref="GetBindingObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
+        /// <param name="o"/>
+        /// <param name="property"/>
+        /// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
+        public static IObservable<BindingValue<TResult>> GetBindingObservable<TSource, TResult>(
+            this AvaloniaObject o,
+            AvaloniaProperty<TSource> property,
+            Func<TSource, TResult> converter)
+        {
+            return new AvaloniaPropertyBindingObservable<TSource, TResult>(
+                o ?? throw new ArgumentNullException(nameof(o)),
+                property ?? throw new ArgumentNullException(nameof(property)),
+                converter ?? throw new ArgumentNullException(nameof(converter)));
+        }
+
         /// <summary>
         /// Gets an observable that listens for property changed events for an
         /// <see cref="AvaloniaProperty"/>.
@@ -338,7 +365,7 @@ namespace Avalonia
                 return InstancedBinding.OneWay(_source);
             }
         }
-        
+
         private class ClassHandlerObserver<TTarget, TValue> : IObserver<AvaloniaPropertyChangedEventArgs<TValue>>
         {
             private readonly Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> _action;

+ 6 - 0
src/Avalonia.Base/AvaloniaProperty.cs

@@ -499,6 +499,12 @@ namespace Avalonia
         /// <param name="o">The object instance.</param>
         internal abstract void RouteClearValue(AvaloniaObject o);
 
+        /// <summary>
+        /// Routes an untyped CoerceValue call on a property with its default value to a typed call.
+        /// </summary>
+        /// <param name="o">The object instance.</param>
+        internal abstract void RouteCoerceDefaultValue(AvaloniaObject o);
+
         /// <summary>
         /// Routes an untyped GetValue call to a typed call.
         /// </summary>

+ 5 - 9
src/Avalonia.Base/CombinedGeometry.cs

@@ -152,19 +152,15 @@ namespace Avalonia.Media
             var g1 = Geometry1;
             var g2 = Geometry2;
 
-            if (g1 is object && g2 is object)
+            if (g1?.PlatformImpl != null && g2?.PlatformImpl != null)
             {
                 var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
-                return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2);
+                return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl);
             }
-            else if (GeometryCombineMode == GeometryCombineMode.Intersect)
-                return null;
-            else if (g1 is object)
-                return g1.PlatformImpl;
-            else if (g2 is object)
-                return g2.PlatformImpl;
-            else
+
+            if (GeometryCombineMode == GeometryCombineMode.Intersect)
                 return null;
+            return g1?.PlatformImpl ?? g2?.PlatformImpl;
         }
     }
 }

+ 21 - 5
src/Avalonia.Base/Controls/ResourceDictionary.cs

@@ -15,6 +15,7 @@ namespace Avalonia.Controls
     /// </summary>
     public class ResourceDictionary : IResourceDictionary
     {
+        private object? lastDeferredItemKey;
         private Dictionary<object, object?>? _inner;
         private IResourceHost? _owner;
         private AvaloniaList<IResourceProvider>? _mergedDictionaries;
@@ -241,12 +242,27 @@ namespace Avalonia.Controls
             {
                 if (value is DeferredItem deffered)
                 {
-                    _inner[key] = value = deffered.Factory(null) switch
+                    // Avoid simple reentrancy, which could commonly occur on redefining the resource.
+                    if (lastDeferredItemKey == key)
                     {
-                        ITemplateResult t => t.Result,
-                        object v => v,
-                        _ => null,
-                    };
+                        value = null;
+                        return false;
+                    }
+
+                    try
+                    {
+                        lastDeferredItemKey = key;
+                        _inner[key] = value = deffered.Factory(null) switch
+                        {
+                            ITemplateResult t => t.Result,
+                            { } v => v,
+                            _ => null,
+                        };
+                    }
+                    finally
+                    {
+                        lastDeferredItemKey = null;
+                    }
                 }
                 return true;
             }

+ 1 - 1
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@@ -40,7 +40,7 @@ namespace Avalonia.Data.Core
         {
             if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj)
             {
-                _subscription = new AvaloniaPropertyObservable<object?>(obj, _property).Subscribe(ValueChanged);
+                _subscription = new AvaloniaPropertyObservable<object?,object?>(obj, _property).Subscribe(ValueChanged);
             }
             else
             {

+ 5 - 0
src/Avalonia.Base/DirectPropertyBase.cs

@@ -117,6 +117,11 @@ namespace Avalonia
             o.ClearValue<TValue>(this);
         }
 
+        internal override void RouteCoerceDefaultValue(AvaloniaObject o)
+        {
+            // Do nothing.
+        }
+
         /// <inheritdoc/>
         internal override object? RouteGetValue(AvaloniaObject o)
         {

+ 30 - 23
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -4,14 +4,17 @@ using Avalonia.Threading;
 
 namespace Avalonia.Input.GestureRecognizers
 {
-    public class ScrollGestureRecognizer 
-        : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
-            IGestureRecognizer
+    public class ScrollGestureRecognizer : AvaloniaObject, IGestureRecognizer
     {
         // Pixels per second speed that is considered to be the stop of inertial scroll
         internal const double InertialScrollSpeedEnd = 5;
         public const double InertialResistance = 0.15;
 
+        private bool _canHorizontallyScroll;
+        private bool _canVerticallyScroll;
+        private bool _isScrollInertiaEnabled;
+        private int _scrollStartDistance = 30;
+
         private bool _scrolling;
         private Point _trackedRootPoint;
         private IPointer? _tracking;
@@ -28,34 +31,39 @@ namespace Avalonia.Input.GestureRecognizers
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
-            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll));
+        public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll), 
+                o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
 
         /// <summary>
         /// Defines the <see cref="CanVerticallyScroll"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
-            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll));
+        public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll),
+                o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v);
 
         /// <summary>
         /// Defines the <see cref="IsScrollInertiaEnabled"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty =
-            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled));
+        public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
+                o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
 
         /// <summary>
         /// Defines the <see cref="ScrollStartDistance"/> property.
         /// </summary>
-        public static readonly StyledProperty<int> ScrollStartDistanceProperty =
-            AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30);
-        
+        public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance),
+                o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v,
+                unsetValue: 30);
+
         /// <summary>
         /// Gets or sets a value indicating whether the content can be scrolled horizontally.
         /// </summary>
         public bool CanHorizontallyScroll
         {
-            get => GetValue(CanHorizontallyScrollProperty);
-            set => SetValue(CanHorizontallyScrollProperty, value);
+            get => _canHorizontallyScroll;
+            set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
         }
 
         /// <summary>
@@ -63,17 +71,17 @@ namespace Avalonia.Input.GestureRecognizers
         /// </summary>
         public bool CanVerticallyScroll
         {
-            get => GetValue(CanVerticallyScrollProperty);
-            set => SetValue(CanVerticallyScrollProperty, value);
+            get => _canVerticallyScroll;
+            set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
         }
-        
+
         /// <summary>
         /// Gets or sets whether the gesture should include inertia in it's behavior.
         /// </summary>
         public bool IsScrollInertiaEnabled
         {
-            get => GetValue(IsScrollInertiaEnabledProperty);
-            set => SetValue(IsScrollInertiaEnabledProperty, value);
+            get => _isScrollInertiaEnabled;
+            set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
         }
 
         /// <summary>
@@ -81,10 +89,9 @@ namespace Avalonia.Input.GestureRecognizers
         /// </summary>
         public int ScrollStartDistance
         {
-            get => GetValue(ScrollStartDistanceProperty);
-            set => SetValue(ScrollStartDistanceProperty, value);
-        }
-        
+            get => _scrollStartDistance;
+            set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
+        }        
 
         public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
         {

+ 18 - 6
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -269,21 +269,25 @@ namespace Avalonia.Layout
             }
         }
 
-        private void Measure(Layoutable control)
+        private bool Measure(Layoutable control)
         {
+            if (!control.IsVisible || !control.IsAttachedToVisualTree)
+                return false;
+
             // Controls closest to the visual root need to be arranged first. We don't try to store
             // ordered invalidation lists, instead we traverse the tree upwards, measuring the
             // controls closest to the root first. This has been shown by benchmarks to be the
             // fastest and most memory-efficient algorithm.
             if (control.VisualParent is Layoutable parent)
             {
-                Measure(parent);
+                if (!Measure(parent))
+                    return false;
             }
 
             // If the control being measured has IsMeasureValid == true here then its measure was
             // handed by an ancestor and can be ignored. The measure may have also caused the
             // control to be removed.
-            if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
+            if (!control.IsMeasureValid)
             {
                 if (control is ILayoutRoot root)
                 {
@@ -294,16 +298,22 @@ namespace Avalonia.Layout
                     control.Measure(control.PreviousMeasure.Value);
                 }
             }
+
+            return true;
         }
 
-        private void Arrange(Layoutable control)
+        private bool Arrange(Layoutable control)
         {
+            if (!control.IsVisible || !control.IsAttachedToVisualTree)
+                return false;
+
             if (control.VisualParent is Layoutable parent)
             {
-                Arrange(parent);
+                if (!Arrange(parent))
+                    return false;
             }
 
-            if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
+            if (control.IsMeasureValid && !control.IsArrangeValid)
             {
                 if (control is IEmbeddedLayoutRoot embeddedRoot)
                     control.Arrange(new Rect(embeddedRoot.AllocatedSize));
@@ -316,6 +326,8 @@ namespace Avalonia.Layout
                     control.Arrange(control.PreviousArrange.Value);
                 }
             }
+
+            return true;
         }
 
         private void QueueLayoutPass()

+ 2 - 2
src/Avalonia.Base/Media/DrawingContext.cs

@@ -132,7 +132,7 @@ namespace Avalonia.Media
             double radiusX = 0, double radiusY = 0,
             BoxShadows boxShadows = default)
         {
-            if (brush == null && !PenIsVisible(pen))
+            if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0)
                 return;
             if (!MathUtilities.IsZero(radiusX))
             {
@@ -160,7 +160,7 @@ namespace Avalonia.Media
         /// </remarks>
         public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
         {
-            if (brush == null && !PenIsVisible(pen))
+            if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0)
                 return;
             DrawRectangleCore(brush, pen, rrect, boxShadows);
         }

+ 45 - 11
src/Avalonia.Base/Media/FontManager.cs

@@ -30,13 +30,15 @@ namespace Avalonia.Media
 
             _fontFallbacks = options?.FontFallbacks;
 
-            DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName();
+            var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName();
 
-            if (string.IsNullOrEmpty(DefaultFontFamilyName))
+            if (string.IsNullOrEmpty(defaultFontFamilyName))
             {
                 throw new InvalidOperationException("Default font family name can't be null or empty.");
             }
 
+            DefaultFontFamily = new FontFamily(defaultFontFamilyName);
+
             AddFontCollection(new SystemFontCollection(this));
         }
 
@@ -65,9 +67,9 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        ///     Gets the system's default font family's name.
+        ///     Gets the system's default font family.
         /// </summary>
-        public string DefaultFontFamilyName
+        public FontFamily DefaultFontFamily
         {
             get;
         }
@@ -93,6 +95,11 @@ namespace Avalonia.Media
 
             var fontFamily = typeface.FontFamily;
 
+            if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
+            {
+                return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
+            }
+
             if (fontFamily.Key is FontFamilyKey key)
             {
                 var source = key.Source;
@@ -131,15 +138,21 @@ namespace Avalonia.Media
                 }
             }
 
-            foreach (var familyName in fontFamily.FamilyNames)
+            for (var i = 0; i < fontFamily.FamilyNames.Count; i++)
             {
+                var familyName = fontFamily.FamilyNames[i];
+
                 if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
                 {
-                    return true;
+                    if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name)
+                    {
+                        return true;
+                    }
                 }
             }
 
-            return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
+            //Nothing was found so use the default
+            return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
         }
 
         /// <summary>
@@ -199,16 +212,37 @@ namespace Avalonia.Media
             {
                 foreach (var fallback in _fontFallbacks)
                 {
-                    typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
+                    if (fallback.UnicodeRange.IsInRange(codepoint))
+                    {
+                        typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
+
+                        if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
 
-                    if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+            //Try to match against fallbacks first
+            if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
+            {
+                for (int i = 1; i < fontFamily.FamilyNames.Count; i++)
+                {
+                    var familyName = fontFamily.FamilyNames[i];
+
+                    foreach (var fontCollection in _fontCollections.Values)
                     {
-                        return true;
+                        if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
+                        {
+                            return true;
+                        };
                     }
                 }
             }
 
-            return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface);
+            //Try to find a match with the system font manager
+            return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
         }
     }
 }

+ 8 - 198
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@@ -8,10 +8,8 @@ using Avalonia.Platform;
 
 namespace Avalonia.Media.Fonts
 {
-    public class EmbeddedFontCollection : IFontCollection
+    public class EmbeddedFontCollection : FontCollectionBase
     {
-        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
-
         private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
 
         private readonly Uri _key;
@@ -25,13 +23,13 @@ namespace Avalonia.Media.Fonts
             _source = source;
         }
 
-        public Uri Key => _key;
+        public override Uri Key => _key;
 
-        public FontFamily this[int index] => _fontFamilies[index];
+        public override FontFamily this[int index] => _fontFamilies[index];
 
-        public int Count => _fontFamilies.Count;
+        public override int Count => _fontFamilies.Count;
 
-        public void Initialize(IFontManagerImpl fontManager)
+        public override void Initialize(IFontManagerImpl fontManager)
         {
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
 
@@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts
                 {
                     if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
                     {
-                        glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
+                        glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
 
                         if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
                         {
@@ -63,27 +61,8 @@ namespace Avalonia.Media.Fonts
             }
         }
 
-        public void Dispose()
-        {
-            foreach (var fontFamily in _fontFamilies)
-            {
-                if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
-                {
-                    foreach (var glyphTypeface in glyphTypefaces.Values)
-                    {
-                        glyphTypeface.Dispose();
-                    }
-                }
-            }
 
-            GC.SuppressFinalize(this);
-        }
-
-        public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
-
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
-        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+        public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
             FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
             var key = new FontCollectionKey(style, weight, stretch);
@@ -116,175 +95,6 @@ namespace Avalonia.Media.Fonts
             return false;
         }
 
-        private static bool TryGetNearestMatch(
-            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
-            FontCollectionKey key,
-            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
-        {
-            if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
-            {
-                return true;
-            }
-
-            if (key.Style != FontStyle.Normal)
-            {
-                key = key with { Style = FontStyle.Normal };
-            }
-
-            if (key.Stretch != FontStretch.Normal)
-            {
-                if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
-                {
-                    return true;
-                }
-
-                if (key.Weight != FontWeight.Normal)
-                {
-                    if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
-                    {
-                        return true;
-                    }
-                }
-
-                key = key with { Stretch = FontStretch.Normal };
-            }
-
-            if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
-            {
-                return true;
-            }
-
-            if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
-            {
-                return true;
-            }
-
-            //Take the first glyph typeface we can find.
-            foreach (var typeface in glyphTypefaces.Values)
-            {
-                glyphTypeface = typeface;
-
-                return true;
-            }
-
-            return false;
-        }
-
-        private static bool TryFindStretchFallback(
-            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
-            FontCollectionKey key,
-            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
-        {
-            glyphTypeface = null;
-
-            var stretch = (int)key.Stretch;
-
-            if (stretch < 5)
-            {
-                for (var i = 0; stretch + i < 9; i++)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-            else
-            {
-                for (var i = 0; stretch - i > 1; i++)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        private static bool TryFindWeightFallback(
-            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
-            FontCollectionKey key,
-            [NotNullWhen(true)] out IGlyphTypeface? typeface)
-        {
-            typeface = null;
-            var weight = (int)key.Weight;
-
-            //If the target weight given is between 400 and 500 inclusive          
-            if (weight >= 400 && weight <= 500)
-            {
-                //Look for available weights between the target and 500, in ascending order.
-                for (var i = 0; weight + i <= 500; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights greater than 500, in ascending order.
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            //If a weight less than 400 is given, look for available weights less than the target, in descending order.           
-            if (weight < 400)
-            {
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
-            if (weight > 500)
-            {
-                for (var i = 0; weight + i <= 900; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-
-                //If no match is found, look for available weights less than the target, in descending order.
-                for (var i = 0; weight - i >= 100; i += 50)
-                {
-                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
+        public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
     }
 }

+ 259 - 0
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@@ -0,0 +1,259 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    public abstract class FontCollectionBase : IFontCollection
+    {
+        protected readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>> _glyphTypefaceCache = new();
+
+        public abstract Uri Key { get; }
+
+        public abstract int Count { get; }
+
+        public abstract FontFamily this[int index] { get; }
+
+        public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
+           [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+
+        public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
+            string? familyName, CultureInfo? culture, out Typeface match)
+        {
+            match = default;
+
+            if (string.IsNullOrEmpty(familyName))
+            {
+                foreach (var typefaces in _glyphTypefaceCache.Values)
+                {
+                    if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface))
+                    {
+                        if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                        {
+                            match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch);
+
+                            return true;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface))
+                {
+                    if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    {
+                        match = new Typeface(familyName, style, weight, stretch);
+
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        public abstract void Initialize(IFontManagerImpl fontManager);
+
+        public abstract IEnumerator<FontFamily> GetEnumerator();
+
+        void IDisposable.Dispose()
+        {
+            foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
+            {
+                foreach (var pair in glyphTypefaces)
+                {
+                    pair.Value?.Dispose();
+                }
+            }
+
+            GC.SuppressFinalize(this);
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        internal static bool TryGetNearestMatch(
+            ConcurrentDictionary<FontCollectionKey,
+            IGlyphTypeface?> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
+            {
+                return true;
+            }
+
+            if (key.Style != FontStyle.Normal)
+            {
+                key = key with { Style = FontStyle.Normal };
+            }
+
+            if (key.Stretch != FontStretch.Normal)
+            {
+                if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+                {
+                    return true;
+                }
+
+                if (key.Weight != FontWeight.Normal)
+                {
+                    if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+
+                key = key with { Stretch = FontStretch.Normal };
+            }
+
+            if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            //Take the first glyph typeface we can find.
+            foreach (var typeface in glyphTypefaces.Values)
+            {
+                if(typeface != null)
+                {
+                    glyphTypeface = typeface;
+
+                    return true;
+                }            
+            }
+
+            return false;
+        }
+
+        internal static bool TryFindStretchFallback(
+            ConcurrentDictionary<FontCollectionKey,
+            IGlyphTypeface?> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            glyphTypeface = null;
+
+            var stretch = (int)key.Stretch;
+
+            if (stretch < 5)
+            {
+                for (var i = 0; stretch + i < 9; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+            }
+            else
+            {
+                for (var i = 0; stretch - i > 1; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        internal static bool TryFindWeightFallback(
+            ConcurrentDictionary<FontCollectionKey,
+            IGlyphTypeface?> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            glyphTypeface = null;
+            var weight = (int)key.Weight;
+
+            //If the target weight given is between 400 and 500 inclusive          
+            if (weight >= 400 && weight <= 500)
+            {
+                //Look for available weights between the target and 500, in ascending order.
+                for (var i = 0; weight + i <= 500; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights greater than 500, in ascending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight less than 400 is given, look for available weights less than the target, in descending order.           
+            if (weight < 400)
+            {
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
+            if (weight > 500)
+            {
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using Avalonia.Platform;
 
 namespace Avalonia.Media.Fonts
@@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts
         /// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
         bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
             FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+
+        /// <summary>
+        ///     Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
+        /// </summary>
+        /// <param name="codepoint">The codepoint to match against.</param>
+        /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontWeight">The font weight.</param>
+        /// <param name="fontStretch">The font stretch.</param>
+        /// <param name="familyName">The family name. This is optional and used for fallback lookup.</param>
+        /// <param name="culture">The culture.</param>
+        /// <param name="typeface">The matching <see cref="Typeface"/>.</param>
+        /// <returns>
+        ///     <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
+        /// </returns>
+        bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
+            FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
     }
 }

+ 14 - 52
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
@@ -7,10 +6,8 @@ using Avalonia.Platform;
 
 namespace Avalonia.Media.Fonts
 {
-    internal class SystemFontCollection : IFontCollection
+    internal class SystemFontCollection : FontCollectionBase
     {
-        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
-
         private readonly FontManager _fontManager;
         private readonly string[] _familyNames;
 
@@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts
             _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
         }
 
-        public Uri Key => FontManager.SystemFontsKey;
+        public override Uri Key => FontManager.SystemFontsKey;
 
-        public FontFamily this[int index]
+        public override FontFamily this[int index]
         {
             get
             {
@@ -32,76 +29,41 @@ namespace Avalonia.Media.Fonts
             }
         }
 
-        public int Count => _familyNames.Length;
+        public override int Count => _familyNames.Length;
 
-        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+        public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
             FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
-            if (familyName == FontFamily.DefaultFontFamilyName)
-            {
-                familyName = _fontManager.DefaultFontFamilyName;
-            }
+            glyphTypeface = null;
 
             var key = new FontCollectionKey(style, weight, stretch);
 
-            if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
-            {
-                if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
-                {
-                    return true;
-                }
-                else
-                {
-                    if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) &&
-                        glyphTypefaces.TryAdd(key, glyphTypeface))
-                    {
-                        return true;
-                    }
-                }
-            }
+            var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>());
 
-            if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+            if (!glyphTypefaces.TryGetValue(key, out glyphTypeface))
             {
-                glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
+                _fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
 
-                if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
+                if (!glyphTypefaces.TryAdd(key, glyphTypeface))
                 {
-                    return true;
+                    return false;
                 }
             }
 
-            return false;
+            return glyphTypeface != null;
         }
 
-        public void Initialize(IFontManagerImpl fontManager)
+        public override void Initialize(IFontManagerImpl fontManager)
         {
             //We initialize the system font collection during construction.
         }
 
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        public IEnumerator<FontFamily> GetEnumerator()
+        public override IEnumerator<FontFamily> GetEnumerator()
         {
             foreach (var familyName in _familyNames)
             {
                 yield return new FontFamily(familyName);
             }
         }
-
-        void IDisposable.Dispose()
-        {
-            foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
-            {
-                foreach (var pair in glyphTypefaces)
-                {
-                    pair.Value.Dispose();
-                }
-            }
-
-            GC.SuppressFinalize(this);
-        }
     }
 }

+ 4 - 1
src/Avalonia.Base/Media/GeometryGroup.cs

@@ -78,7 +78,10 @@ namespace Avalonia.Media
             {
                 var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
 
-                return factory.CreateGeometryGroup(FillRule, _children);
+                var children = new IGeometryImpl?[_children.Count];
+                for (var c = 0; c < _children.Count; c++)
+                    children[c] = _children[c].PlatformImpl;
+                return factory.CreateGeometryGroup(FillRule, children!);
             }
 
             return null;

+ 67 - 25
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -57,21 +57,21 @@ namespace Avalonia.Media.TextFormatting
                 switch (paragraphProperties.TextWrapping)
                 {
                     case TextWrapping.NoWrap:
-                    {
-                        var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
-                            textSourceLength,
-                            paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
+                        {
+                            var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
+                                textSourceLength,
+                                paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
                             textLine.FinalizeLine();
 
-                        return textLine;
-                    }
+                            return textLine;
+                        }
                     case TextWrapping.WrapWithOverflow:
                     case TextWrapping.Wrap:
-                    {
-                        return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
-                            paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
-                    }
+                        {
+                            return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
+                                paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
+                        }
                     default:
                         throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping));
                 }
@@ -568,9 +568,9 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength)
+        private static int MeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth)
         {
-            measuredLength = 0;
+            var measuredLength = 0;
             var currentWidth = 0.0;
 
             for (var i = 0; i < textRuns.Count; ++i)
@@ -583,25 +583,59 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
-                                var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster;
-                                var lastCluster = firstCluster;
+                                var runLength = 0;
 
                                 for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)
                                 {
-                                    var glyphInfo = shapedTextCharacters.ShapedBuffer[j];
+                                    var currentInfo = shapedTextCharacters.ShapedBuffer[j];
+
+                                    var clusterWidth = currentInfo.GlyphAdvance;
 
-                                    if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+                                    GlyphInfo nextInfo = default;
+
+                                    while (j + 1 < shapedTextCharacters.ShapedBuffer.Length)
                                     {
-                                        measuredLength += Math.Max(0, lastCluster - firstCluster);
+                                        nextInfo = shapedTextCharacters.ShapedBuffer[j + 1];
+
+                                        if (currentInfo.GlyphCluster == nextInfo.GlyphCluster)
+                                        {
+                                            clusterWidth += nextInfo.GlyphAdvance;
+
+                                            j++;
+
+                                            continue;
+                                        }
+
+                                        break;
+                                    }                              
 
-                                        return measuredLength != 0;
+                                    var clusterLength = Math.Max(0, nextInfo.GlyphCluster - currentInfo.GlyphCluster);
+
+                                    if(clusterLength == 0)
+                                    {
+                                        clusterLength = currentRun.Length - runLength;
+                                    }
+
+                                    if(clusterLength == 0)
+                                    {
+                                        clusterLength = shapedTextCharacters.GlyphRun.Metrics.FirstCluster + currentRun.Length - currentInfo.GlyphCluster;
+                                    }   
+
+                                    if (currentWidth + clusterWidth > paragraphWidth)
+                                    {
+                                        if (runLength == 0 && measuredLength == 0)
+                                        {
+                                            runLength = clusterLength;
+                                        }
+
+                                        return measuredLength + runLength;
                                     }
 
-                                    lastCluster = glyphInfo.GlyphCluster;
-                                    currentWidth += glyphInfo.GlyphAdvance;
+                                    currentWidth += clusterWidth;
+                                    runLength += clusterLength;
                                 }
 
-                                measuredLength += currentRun.Length;
+                                measuredLength += runLength;
                             }
 
                             break;
@@ -611,7 +645,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
                             {
-                                return measuredLength != 0;
+                                return measuredLength;
                             }
 
                             measuredLength += currentRun.Length;
@@ -628,7 +662,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return measuredLength != 0;
+            return measuredLength;
         }
 
         /// <summary>
@@ -675,9 +709,11 @@ namespace Avalonia.Media.TextFormatting
                 return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
             }
 
-            if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
+            var measuredLength = MeasureLength(textRuns, paragraphWidth);
+
+            if(measuredLength == 0)
             {
-                measuredLength = 1;
+
             }
 
             var currentLength = 0;
@@ -798,6 +834,12 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
+                //We don't want to surpass the measuredLength with trailing whitespace when we are in a right to left setting.
+                if(currentPosition > measuredLength && resolvedFlowDirection == FlowDirection.RightToLeft)
+                {
+                    break;
+                }
+
                 measuredLength = currentPosition;
 
                 break;

+ 16 - 2
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -701,7 +701,14 @@ namespace Avalonia.Media.TextFormatting
                     if (directionalWidth == 0)
                     {
                         //In case a run only contains a linebreak we don't want to skip it.
-                        if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
+                        if (currentRun is ShapedTextRun shaped)
+                        {
+                            if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
+                            {
+                                continue;
+                            }
+                        }
+                        else
                         {
                             continue;
                         }
@@ -840,7 +847,14 @@ namespace Avalonia.Media.TextFormatting
                     if (directionalWidth == 0)
                     {
                         //In case a run only contains a linebreak we don't want to skip it.
-                        if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
+                        if (currentRun is ShapedTextRun shaped)
+                        {
+                            if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
+                            {
+                                continue;
+                            }
+                        }
+                        else
                         {
                             continue;
                         }

+ 1 - 3
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@@ -27,15 +27,13 @@ namespace Avalonia.Platform
         /// <param name="fontStyle">The font style.</param>
         /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontStretch">The font stretch.</param>
-        /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
         /// <param name="culture">The culture.</param>
         /// <param name="typeface">The matching typeface.</param>
         /// <returns>
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
         /// </returns>
         bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
-            FontWeight fontWeight, FontStretch fontStretch,
-            FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
+            FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface);
 
         /// <summary>
         ///     Tries to get a glyph typeface for specified parameters.

+ 2 - 2
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -48,7 +48,7 @@ namespace Avalonia.Platform
         /// <param name="fillRule">The fill rule.</param>
         /// <param name="children">The geometries to group.</param>
         /// <returns>A combined geometry.</returns>
-        IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children);
+        IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children);
 
         /// <summary>
         /// Creates a geometry group implementation.
@@ -57,7 +57,7 @@ namespace Avalonia.Platform
         /// <param name="g1">The first geometry.</param>
         /// <param name="g2">The second geometry.</param>
         /// <returns>A combined geometry.</returns>
-        IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
+        IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2);
 
         /// <summary>
         /// Created a geometry implementation for the glyph run.

+ 47 - 2
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@@ -36,12 +36,23 @@ namespace Avalonia.PropertyStore
         /// </summary>
         public IValueEntry? BaseValueEntry { get; private set; }
 
+        /// <summary>
+        /// Gets a value indicating whether the property has a coercion function.
+        /// </summary>
+        public bool HasCoercion { get; protected set; }
+
         /// <summary>
         /// Gets a value indicating whether the <see cref="Value"/> was overridden by a call to 
         /// <see cref="AvaloniaObject.SetCurrentValue{T}"/>.
         /// </summary>
         public bool IsOverridenCurrentValue { get; set; }
 
+        /// <summary>
+        /// Gets a value indicating whether the <see cref="Value"/> is the result of the 
+        /// 
+        /// </summary>
+        public bool IsCoercedDefaultValue { get; set; }
+
         /// <summary>
         /// Begins a reevaluation pass on the effective value.
         /// </summary>
@@ -63,10 +74,33 @@ namespace Avalonia.PropertyStore
         /// <summary>
         /// Ends a reevaluation pass on the effective value.
         /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="property">The property being reevaluated.</param>
         /// <remarks>
-        /// This method unsubscribes from any unused value entries.
+        /// Handles coercing the default value if necessary.
         /// </remarks>
-        public void EndReevaluation()
+        public void EndReevaluation(ValueStore owner, AvaloniaProperty property)
+        {
+            if (Priority == BindingPriority.Unset && HasCoercion)
+                CoerceDefaultValueAndRaise(owner, property);
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the effective value represents the default value of the
+        /// property and can be removed.
+        /// </summary>
+        /// <returns>True if the effective value van be removed; otherwise false.</returns>
+        public bool CanRemove()
+        {
+            return Priority == BindingPriority.Unset &&
+                !IsOverridenCurrentValue &&
+                !IsCoercedDefaultValue;
+        }
+
+        /// <summary>
+        /// Unsubscribes from any unused value entries.
+        /// </summary>
+        public void UnsubscribeIfNecessary()
         {
             if (Priority == BindingPriority.Unset)
             {
@@ -130,6 +164,17 @@ namespace Avalonia.PropertyStore
         /// <param name="property">The property being cleared.</param>
         public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property);
 
+        /// <summary>
+        /// Coerces the default value, raising <see cref="AvaloniaObject.PropertyChanged"/>
+        /// where necessary.
+        /// </summary>
+        /// <param name="owner">The associated value store.</param>
+        /// <param name="property">The property being coerced.</param>
+        protected abstract void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property);
+
+        /// <summary>
+        /// Gets the current effective value as a boxed value.
+        /// </summary>
         protected abstract object? GetBoxedValue();
 
         protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority)

+ 35 - 19
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
-using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
 
 namespace Avalonia.PropertyStore
 {
@@ -33,19 +32,16 @@ namespace Avalonia.PropertyStore
 
             if (_metadata.CoerceValue is { } coerce)
             {
+                HasCoercion = true;
                 _uncommon = new()
                 {
                     _coerce = coerce,
                     _uncoercedValue = value,
                     _uncoercedBaseValue = value,
                 };
-
-                Value = coerce(owner, value);
-            }
-            else
-            {
-                Value = value;
             }
+
+            Value = value;
         }
 
         /// <summary>
@@ -61,7 +57,7 @@ namespace Avalonia.PropertyStore
             Debug.Assert(priority != BindingPriority.LocalValue);
             UpdateValueEntry(value, priority);
 
-            SetAndRaiseCore(owner,  (StyledProperty<T>)value.Property, GetValue(value), priority, false);
+            SetAndRaiseCore(owner,  (StyledProperty<T>)value.Property, GetValue(value), priority);
 
             if (priority > BindingPriority.LocalValue &&
                 value.GetDataValidationState(out var state, out var error))
@@ -75,7 +71,7 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             T value)
         {
-            SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false);
+            SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
         }
 
         public void SetCurrentValueAndRaise(
@@ -83,8 +79,15 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             T value)
         {
-            IsOverridenCurrentValue = true;
-            SetAndRaiseCore(owner, property, value, Priority, true);
+            SetAndRaiseCore(owner, property, value, Priority, isOverriddenCurrentValue: true);
+        }
+
+        public void SetCoercedDefaultValueAndRaise(
+            ValueStore owner,
+            StyledProperty<T> property,
+            T value)
+        {
+            SetAndRaiseCore(owner, property, value, Priority, isCoercedDefaultValue: true);
         }
 
         public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
@@ -117,7 +120,7 @@ namespace Avalonia.PropertyStore
             Debug.Assert(Priority != BindingPriority.Animation);
             Debug.Assert(BasePriority != BindingPriority.Unset);
             UpdateValueEntry(null, BindingPriority.Animation);
-            SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority, false);
+            SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority);
         }
 
         public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@@ -140,24 +143,24 @@ namespace Avalonia.PropertyStore
 
             var p = (StyledProperty<T>)property;
             BindingPriority priority;
-            T oldValue;
+            T newValue;
 
             if (property.Inherits && owner.TryGetInheritedValue(property, out var i))
             {
-                oldValue = ((EffectiveValue<T>)i).Value;
+                newValue = ((EffectiveValue<T>)i).Value;
                 priority = BindingPriority.Inherited;
             }
             else
             {
-                oldValue = _metadata.DefaultValue;
+                newValue = _metadata.DefaultValue;
                 priority = BindingPriority.Unset;
             }
 
-            if (!EqualityComparer<T>.Default.Equals(oldValue, Value))
+            if (!EqualityComparer<T>.Default.Equals(newValue, Value))
             {
-                owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true);
+                owner.Owner.RaisePropertyChanged(p, Value, newValue, priority, true);
                 if (property.Inherits)
-                    owner.OnInheritedEffectiveValueDisposed(p, Value);
+                    owner.OnInheritedEffectiveValueDisposed(p, Value, newValue);
             }
 
             if (ValueEntry?.GetDataValidationState(out _, out _) ??
@@ -168,6 +171,17 @@ namespace Avalonia.PropertyStore
             }
         }
 
+        protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)
+        {
+            Debug.Assert(_uncommon?._coerce is not null);
+            Debug.Assert(Priority == BindingPriority.Unset);
+
+            var coercedDefaultValue = _uncommon!._coerce!(owner.Owner, _metadata.DefaultValue);
+
+            if (!EqualityComparer<T>.Default.Equals(_metadata.DefaultValue, coercedDefaultValue))
+                SetCoercedDefaultValueAndRaise(owner, (StyledProperty<T>)property, coercedDefaultValue);
+        }
+
         protected override object? GetBoxedValue() => Value;
         
         private static T GetValue(IValueEntry entry)
@@ -183,7 +197,8 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             T value,
             BindingPriority priority,
-            bool isOverriddenCurrentValue)
+            bool isOverriddenCurrentValue = false,
+            bool isCoercedDefaultValue = false)
         {
             var oldValue = Value;
             var valueChanged = false;
@@ -191,6 +206,7 @@ namespace Avalonia.PropertyStore
             var v = value;
 
             IsOverridenCurrentValue = isOverriddenCurrentValue;
+            IsCoercedDefaultValue = isCoercedDefaultValue;
 
             if (_uncommon?._coerce is { } coerce)
                 v = coerce(owner.Owner, value);

+ 45 - 15
src/Avalonia.Base/PropertyStore/ValueStore.cs

@@ -259,6 +259,27 @@ namespace Avalonia.PropertyStore
         {
             if (_effectiveValues.TryGetValue(property, out var v))
                 v.CoerceValue(this, property);
+            else
+                property.RouteCoerceDefaultValue(Owner);
+        }
+
+        public void CoerceDefaultValue<T>(StyledProperty<T> property)
+        {
+            var metadata = property.GetMetadata(Owner.GetType());
+
+            if (metadata.CoerceValue is null)
+                return;
+
+            var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue);
+
+            if (EqualityComparer<T>.Default.Equals(metadata.DefaultValue, coercedDefaultValue))
+                return;
+
+            // We have a situation where the default value isn't valid according to the coerce
+            // function. In this case, we need to create an EffectiveValue entry.
+            var effectiveValue = CreateEffectiveValue(property);
+            AddEffectiveValue(property, effectiveValue);
+            effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue);
         }
 
         public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
@@ -419,7 +440,9 @@ namespace Avalonia.PropertyStore
                 ReevaluateEffectiveValue(property, current);
             }
             else
+            {
                 ReevaluateEffectiveValues();
+            }
         }
 
         /// <summary>
@@ -481,7 +504,8 @@ namespace Avalonia.PropertyStore
         /// </summary>
         /// <param name="property">The property whose value changed.</param>
         /// <param name="oldValue">The old value of the property.</param>
-        public void OnInheritedEffectiveValueDisposed<T>(StyledProperty<T> property, T oldValue)
+        /// <param name="newValue">The new value of the property.</param>
+        public void OnInheritedEffectiveValueDisposed<T>(StyledProperty<T> property, T oldValue, T newValue)
         {
             Debug.Assert(property.Inherits);
 
@@ -489,12 +513,11 @@ namespace Avalonia.PropertyStore
 
             if (children is not null)
             {
-                var defaultValue = property.GetDefaultValue(Owner.GetType());
                 var count = children.Count;
 
                 for (var i = 0; i < count; ++i)
                 {
-                    children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue);
+                    children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
                 }
             }
         }
@@ -838,20 +861,25 @@ namespace Avalonia.PropertyStore
                         goto restart;
                 }
 
-                if (current?.Priority == BindingPriority.Unset)
+                if (current is not null)
                 {
-                    if (current.BasePriority == BindingPriority.Unset)
-                    {
-                        RemoveEffectiveValue(property);
-                        current.DisposeAndRaiseUnset(this, property);
-                    }
-                    else
+                    current.EndReevaluation(this, property);
+
+                    if (current.CanRemove())
                     {
-                        current.RemoveAnimationAndRaise(this, property);
+                        if (current.BasePriority == BindingPriority.Unset)
+                        {
+                            RemoveEffectiveValue(property);
+                            current.DisposeAndRaiseUnset(this, property);
+                        }
+                        else
+                        {
+                            current.RemoveAnimationAndRaise(this, property);
+                        }
                     }
-                }
 
-                current?.EndReevaluation();
+                    current.UnsubscribeIfNecessary();
+                }
             }
             finally
             {
@@ -923,7 +951,9 @@ namespace Avalonia.PropertyStore
                 {
                     _effectiveValues.GetKeyValue(i, out var key, out var e);
 
-                    if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue)
+                    e.EndReevaluation(this, key);
+
+                    if (e.CanRemove())
                     {
                         RemoveEffectiveValue(key, i);
                         e.DisposeAndRaiseUnset(this, key);
@@ -932,7 +962,7 @@ namespace Avalonia.PropertyStore
                             break;
                     }
 
-                    e.EndReevaluation();
+                    e.UnsubscribeIfNecessary();
                 }
             }
             finally

+ 68 - 17
src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs

@@ -4,18 +4,21 @@ using Avalonia.Data;
 
 namespace Avalonia.Reactive
 {
-    internal class AvaloniaPropertyBindingObservable<T> : LightweightObservableBase<BindingValue<T>>, IDescription
+    internal class AvaloniaPropertyBindingObservable<TSource,TResult> : LightweightObservableBase<BindingValue<TResult>>, IDescription
     {
         private readonly WeakReference<AvaloniaObject> _target;
         private readonly AvaloniaProperty _property;
-        private BindingValue<T> _value = BindingValue<T>.Unset;
+        private readonly Func<TSource, TResult>? _converter;
+        private BindingValue<TResult> _value = BindingValue<TResult>.Unset;
 
         public AvaloniaPropertyBindingObservable(
             AvaloniaObject target,
-            AvaloniaProperty property)
+            AvaloniaProperty property,
+            Func<TSource, TResult>? converter = null)
         {
             _target = new WeakReference<AvaloniaObject>(target);
             _property = property;
+            _converter = converter;
         }
 
         public string Description => $"{_target.GetType().Name}.{_property.Name}";
@@ -24,8 +27,17 @@ namespace Avalonia.Reactive
         {
             if (_target.TryGetTarget(out var target))
             {
-                _value = (T)target.GetValue(_property)!;
-                target.PropertyChanged += PropertyChanged;
+                if (_converter is { } converter)
+                {
+                    var unconvertedValue = (TSource)target.GetValue(_property)!;
+                    _value = converter(unconvertedValue);
+                    target.PropertyChanged += PropertyChanged_WithConversion;
+                }
+                else
+                {
+                    _value = (TResult)target.GetValue(_property)!;
+                    target.PropertyChanged += PropertyChanged;
+                }
             }
         }
 
@@ -33,11 +45,18 @@ namespace Avalonia.Reactive
         {
             if (_target.TryGetTarget(out var target))
             {
-                target.PropertyChanged -= PropertyChanged;
+                if (_converter is not null)
+                {
+                    target.PropertyChanged -= PropertyChanged_WithConversion;
+                }
+                else
+                {
+                    target.PropertyChanged -= PropertyChanged;
+                }
             }
         }
 
-        protected override void Subscribed(IObserver<BindingValue<T>> observer, bool first)
+        protected override void Subscribed(IObserver<BindingValue<TResult>> observer, bool first)
         {
             if (_value.Type != BindingValueType.UnsetValue)
             {
@@ -49,27 +68,59 @@ namespace Avalonia.Reactive
         {
             if (e.Property == _property)
             {
-                if (e is AvaloniaPropertyChangedEventArgs<T> typedArgs)
+                if (e is AvaloniaPropertyChangedEventArgs<TResult> typedArgs)
                 {
-                    var newValue = e.Sender.GetValue<T>(typedArgs.Property);
+                    PublishValue(e.Sender.GetValue<TResult>(typedArgs.Property));
+                }
+                else
+                {
+                    PublishUntypedValue(e.Sender.GetValue(e.Property));
+                }
+            }
+        }
 
-                    if (!_value.HasValue || !EqualityComparer<T>.Default.Equals(newValue, _value.Value))
-                    {
-                        _value = newValue;
-                        PublishNext(_value);
-                    }
+        private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == _property)
+            {
+                if (e is AvaloniaPropertyChangedEventArgs<TSource> typedArgs)
+                {
+                    var newValueRaw = e.Sender.GetValue<TSource>(typedArgs.Property);
+
+                    var newValue = _converter!(newValueRaw);
+
+                    PublishValue(newValue);
                 }
                 else
                 {
                     var newValue = e.Sender.GetValue(e.Property);
 
-                    if (!Equals(newValue, _value))
+                    if (newValue is TSource source)
                     {
-                        _value = (T)newValue!;
-                        PublishNext(_value);
+                        newValue = _converter!(source);
                     }
+
+                    PublishUntypedValue(newValue);
                 }
             }
         }
+
+        private void PublishValue(TResult newValue)
+        {
+            if (!_value.HasValue || !EqualityComparer<TResult>.Default.Equals(newValue, _value.Value))
+            {
+                _value = newValue;
+                PublishNext(_value);
+            }
+        }
+
+        private void PublishUntypedValue(object? newValue)
+        {
+            if (!Equals(newValue, _value))
+            {
+                _value = (TResult)newValue!;
+                PublishNext(_value);
+            }
+        }
     }
 }

+ 59 - 14
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@@ -4,18 +4,21 @@ using Avalonia.Data;
 
 namespace Avalonia.Reactive
 {
-    internal class AvaloniaPropertyObservable<T> : LightweightObservableBase<T>, IDescription
+    internal class AvaloniaPropertyObservable<TSource,TResult> : LightweightObservableBase<TResult>, IDescription
     {
         private readonly WeakReference<AvaloniaObject> _target;
         private readonly AvaloniaProperty _property;
-        private Optional<T> _value;
+        private readonly Func<TSource, TResult>? _converter;
+        private Optional<TResult> _value;
 
         public AvaloniaPropertyObservable(
             AvaloniaObject target,
-            AvaloniaProperty property)
+            AvaloniaProperty property,
+            Func<TSource,TResult>? converter = null)
         {
             _target = new WeakReference<AvaloniaObject>(target);
             _property = property;
+            _converter = converter;
         }
 
         public string Description => $"{_target.GetType().Name}.{_property.Name}";
@@ -24,8 +27,17 @@ namespace Avalonia.Reactive
         {
             if (_target.TryGetTarget(out var target))
             {
-                _value = (T)target.GetValue(_property)!;
-                target.PropertyChanged += PropertyChanged;
+                if (_converter is { } converter)
+                {
+                    var unconvertedValue = (TSource)target.GetValue(_property)!;
+                    _value = converter(unconvertedValue);
+                    target.PropertyChanged += PropertyChanged_WithConversion;
+                }
+                else
+                {
+                    _value = (TResult)target.GetValue(_property)!;
+                    target.PropertyChanged += PropertyChanged;
+                }
             }
         }
 
@@ -33,13 +45,20 @@ namespace Avalonia.Reactive
         {
             if (_target.TryGetTarget(out var target))
             {
-                target.PropertyChanged -= PropertyChanged;
+                if (_converter is not null)
+                {
+                    target.PropertyChanged -= PropertyChanged_WithConversion;
+                }
+                else
+                {
+                    target.PropertyChanged -= PropertyChanged;
+                }
             }
 
             _value = default;
         }
 
-        protected override void Subscribed(IObserver<T> observer, bool first)
+        protected override void Subscribed(IObserver<TResult> observer, bool first)
         {
             if (_value.HasValue)
                 observer.OnNext(_value.Value);
@@ -49,23 +68,49 @@ namespace Avalonia.Reactive
         {
             if (e.Property == _property)
             {
-                T newValue;
+                TResult newValue;
 
-                if (e is AvaloniaPropertyChangedEventArgs<T> typed)
+                if (e is AvaloniaPropertyChangedEventArgs<TResult> typed)
                 {
                     newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property);
                 }
                 else
                 {
-                    newValue = (T)e.Sender.GetValue(e.Property)!;
+                    newValue = (TResult)e.Sender.GetValue(e.Property)!;
                 }
 
-                if (!_value.HasValue ||
-                    !EqualityComparer<T>.Default.Equals(newValue, _value.Value))
+                PublishNewValue(newValue);
+            }
+        }
+
+        private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == _property)
+            {
+                TSource newValueRaw;
+
+                if (e is AvaloniaPropertyChangedEventArgs<TSource> typed)
                 {
-                    _value = newValue;
-                    PublishNext(_value.Value!);
+                    newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property);
                 }
+                else
+                {
+                    newValueRaw = (TSource)e.Sender.GetValue(e.Property)!;
+                }
+
+                var newValue = _converter!(newValueRaw);
+
+                PublishNewValue(newValue);
+            }
+        }
+
+        private void PublishNewValue(TResult newValue)
+        {
+            if (!_value.HasValue ||
+                !EqualityComparer<TResult>.Default.Equals(newValue, _value.Value))
+            {
+                _value = newValue;
+                PublishNext(_value.Value!);
             }
         }
     }

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Rendering.SceneGraph
             Custom = custom;
         }
 
-        public override bool HitTest(Point p) => Custom.HitTest(p);
+        public override bool HitTestTransformed(Point p) => Custom.HitTest(p);
 
         public override void Render(IDrawingContextImpl context)
         {

+ 15 - 0
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@@ -37,5 +37,20 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         public Matrix Transform { get; }
+
+        public sealed override bool HitTest(Point p)
+        {
+            if (Transform.IsIdentity)
+                return HitTestTransformed(p);
+
+            if (!Transform.HasInverse)
+                return false;
+
+            var transformedPoint = Transform.Invert().Transform(p);
+
+            return HitTestTransformed(transformedPoint);
+        }
+
+        public abstract bool HitTestTransformed(Point p);
     }
 }

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@@ -43,7 +43,7 @@ namespace Avalonia.Rendering.SceneGraph
 
         public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect);
 
-        public override bool HitTest(Point p)
+        public override bool HitTestTransformed(Point p)
         {
             var center = Rect.Center;
 

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@@ -65,6 +65,6 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p);
+        public override bool HitTestTransformed(Point p) => Rect.Rect.ContainsExclusive(p);
     }
 }

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@@ -64,7 +64,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p)
+        public override bool HitTestTransformed(Point p)
         {
             return (Brush != null && Geometry.FillContains(p)) ||
                    (Pen != null && Geometry.StrokeContains(Pen, p));

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@@ -53,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
+        public override bool HitTestTransformed(Point p) => Bounds.ContainsExclusive(p);
 
         public override void Dispose()
         {

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@@ -94,7 +94,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
+        public override bool HitTestTransformed(Point p) => DestRect.ContainsExclusive(p);
 
         public override void Dispose()
         {

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@@ -66,7 +66,7 @@ namespace Avalonia.Rendering.SceneGraph
             context.DrawLine(Pen, P1, P2);
         }
 
-        public override bool HitTest(Point p)
+        public override bool HitTestTransformed(Point p)
         {
             var halfThickness = Pen.Thickness / 2;
             var minX = Math.Min(P1.X, P2.X) - halfThickness;

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph
 
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p) => false;
+        public override bool HitTestTransformed(Point p) => false;
 
         /// <summary>
         /// Determines if this draw operation equals another.

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph
         public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
 
         /// <inheritdoc/>
-        public override bool HitTest(Point p)
+        public override bool HitTestTransformed(Point p)
         {
             if (Brush != null)
             {

+ 1 - 1
src/Avalonia.Base/StyledElement.cs

@@ -891,7 +891,7 @@ namespace Avalonia
 
             for (var i = 0; i < logicalChildrenCount; i++)
             {
-                if (logicalChildren[i] is StyledElement child)
+                if (logicalChildren[i] is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler
                 {
                     child.OnAttachedToLogicalTreeCore(e);
                 }

+ 5 - 0
src/Avalonia.Base/StyledProperty.cs

@@ -176,6 +176,11 @@ namespace Avalonia
             o.ClearValue<TValue>(this);
         }
 
+        internal override void RouteCoerceDefaultValue(AvaloniaObject o)
+        {
+            o.GetValueStore().CoerceDefaultValue(this);
+        }
+
         /// <inheritdoc/>
         internal override object? RouteGetValue(AvaloniaObject o)
         {

+ 4 - 4
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@@ -557,10 +557,10 @@ public partial class Dispatcher
     /// <returns>
     ///     An task that completes after the task returned from callback finishes
     /// </returns>
-    public Task InvokeTaskAsync(Func<Task> callback, DispatcherPriority priority = default)
+    public Task InvokeAsync(Func<Task> callback, DispatcherPriority priority = default)
     {
         _ = callback ?? throw new ArgumentNullException(nameof(callback));
-        return InvokeAsync(callback, priority).GetTask().Unwrap();
+        return InvokeAsync<Task>(callback, priority).GetTask().Unwrap();
     }
     
     /// <summary>
@@ -578,10 +578,10 @@ public partial class Dispatcher
     /// <returns>
     ///     An task that completes after the task returned from callback finishes
     /// </returns>
-    public Task<TResult> InvokeTaskAsync<TResult>(Func<Task<TResult>> action, DispatcherPriority priority = default)
+    public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> action, DispatcherPriority priority = default)
     {
         _ = action ?? throw new ArgumentNullException(nameof(action));
-        return InvokeAsync(action, priority).GetTask().Unwrap();
+        return InvokeAsync<Task<TResult>>(action, priority).GetTask().Unwrap();
     }
 
     /// <summary>

+ 1 - 1
src/Avalonia.Base/Visual.cs

@@ -487,7 +487,7 @@ namespace Avalonia
 
             for (var i = 0; i < visualChildrenCount; i++)
             {
-                if (visualChildren[i] is { } child)
+                if (visualChildren[i] is { } child && child._visualRoot != e.Root) // child may already have been attached within an event handler
                 {
                     child.OnAttachedToVisualTreeCore(e);
                 }

+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs → src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs


+ 1 - 1
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs

@@ -48,7 +48,7 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<AlphaComponentPosition> HexInputAlphaPositionProperty =
             AvaloniaProperty.Register<ColorView, AlphaComponentPosition>(
                 nameof(HexInputAlphaPosition),
-                AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI
+                AlphaComponentPosition.Leading); // By default match XAML and the WinUI control
 
         /// <summary>
         /// Defines the <see cref="HsvColor"/> property.

+ 5 - 1
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs

@@ -61,7 +61,11 @@ namespace Avalonia.Controls
         {
             if (_hexTextBox != null)
             {
-                _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition);
+                _hexTextBox.Text = ColorToHexConverter.ToHexString(
+                    Color,
+                    HexInputAlphaPosition,
+                    includeAlpha: (IsAlphaEnabled && IsAlphaVisible),
+                    includeSymbol: false);
             }
         }
 

+ 33 - 7
src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs

@@ -11,6 +11,18 @@ namespace Avalonia.Controls.Converters
     /// </summary>
     public class ColorToHexConverter : IValueConverter
     {
+        /// <summary>
+        /// Gets or sets a value indicating whether the alpha component is visible in the Hex formatted text.
+        /// </summary>
+        /// <remarks>
+        /// When hidden the existing alpha component value is maintained. Also when hidden the user is still
+        /// able to input an 8-digit number with alpha. Alpha will be processed but then removed when displayed.
+        ///
+        /// Because this property only controls whether alpha is displayed (and it is still processed regardless)
+        /// it is termed 'Visible' instead of 'Enabled'.
+        /// </remarks>
+        public bool IsAlphaVisible { get; set; } = true;
+
         /// <summary>
         /// Gets or sets the position of a color's alpha component relative to all other components.
         /// </summary>
@@ -48,7 +60,7 @@ namespace Avalonia.Controls.Converters
                 return AvaloniaProperty.UnsetValue;
             }
 
-            return ToHexString(color, AlphaPosition, includeSymbol);
+            return ToHexString(color, AlphaPosition, IsAlphaVisible, includeSymbol);
         }
 
         /// <inheritdoc/>
@@ -67,26 +79,40 @@ namespace Avalonia.Controls.Converters
         /// </summary>
         /// <param name="color">The color to represent as a hex value string.</param>
         /// <param name="alphaPosition">The output position of the alpha component.</param>
+        /// <param name="includeAlpha">Whether the alpha component will be included in the hex string.</param>
         /// <param name="includeSymbol">Whether the hex symbol '#' will be added.</param>
         /// <returns>The input color converted to its hex value string.</returns>
         public static string ToHexString(
             Color color,
             AlphaComponentPosition alphaPosition,
+            bool includeAlpha = true,
             bool includeSymbol = false)
         {
             uint intColor;
-            if (alphaPosition == AlphaComponentPosition.Trailing)
+            string hexColor;
+
+            if (includeAlpha)
             {
-                intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A;
+                if (alphaPosition == AlphaComponentPosition.Trailing)
+                {
+                    intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A;
+                }
+                else
+                {
+                    // Default is Leading alpha (same as XAML)
+                    intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B;
+                }
+
+                hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
             }
             else
             {
-                // Default is Leading alpha
-                intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B;
+                // In this case the alpha position no longer matters
+                // Both cases are calculated the same
+                intColor = ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B;
+                hexColor = intColor.ToString("x6", CultureInfo.InvariantCulture).ToUpperInvariant();
             }
 
-            string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
-
             if (includeSymbol)
             {
                 hexColor = '#' + hexColor;

+ 2 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -6,6 +6,8 @@
   <ControlTheme x:Key="{x:Type ColorPicker}"
                 TargetType="ColorPicker">
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <!-- Alpha position should match CSS (and default slider order) instead of XAML/WinUI -->
+    <Setter Property="HexInputAlphaPosition" Value="Trailing" />
     <Setter Property="Height" Value="32" />
     <Setter Property="Width" Value="64" />
     <Setter Property="MinWidth" Value="64" />

+ 1 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@@ -64,6 +64,7 @@
             <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
+                    Background="Transparent"
                     BoxShadow="0 0 10 2 #BF000000"
                     CornerRadius="{TemplateBinding CornerRadius}"
                     Margin="10">

+ 2 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -295,6 +295,8 @@
   <ControlTheme x:Key="{x:Type ColorView}"
                 TargetType="ColorView">
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <!-- Alpha position should match CSS (and default slider order) instead of XAML/WinUI -->
+    <Setter Property="HexInputAlphaPosition" Value="Trailing" />
     <Setter Property="Palette">
       <controls:FluentColorPalette />
     </Setter>

+ 2 - 0
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@@ -6,6 +6,8 @@
   <ControlTheme x:Key="{x:Type ColorPicker}"
                 TargetType="ColorPicker">
     <Setter Property="CornerRadius" Value="0" />
+    <!-- Alpha position should match CSS (and default slider order) instead of XAML/WinUI -->
+    <Setter Property="HexInputAlphaPosition" Value="Trailing" />
     <Setter Property="Height" Value="32" />
     <Setter Property="Width" Value="64" />
     <Setter Property="MinWidth" Value="64" />

+ 1 - 0
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml

@@ -64,6 +64,7 @@
             <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
+                    Background="Transparent"
                     BoxShadow="0 0 10 2 #BF000000"
                     CornerRadius="{TemplateBinding CornerRadius}"
                     Margin="10">

+ 2 - 0
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@@ -257,6 +257,8 @@
   <ControlTheme x:Key="{x:Type ColorView}"
                 TargetType="ColorView">
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <!-- Alpha position should match CSS (and default slider order) instead of XAML/WinUI -->
+    <Setter Property="HexInputAlphaPosition" Value="Trailing" />
     <Setter Property="Palette">
       <controls:FluentColorPalette />
     </Setter>

+ 4 - 7
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -440,13 +440,10 @@ namespace Avalonia.Controls
             }
 
             Debug.Assert(OwningGrid.Parent is InputElement);
-
-            double distanceFromLeft = mousePosition.X;
-            double distanceFromRight = Bounds.Width - distanceFromLeft;
-
+            
             OnMouseMove_Resize(ref handled, mousePositionHeaders);
 
-            OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight);
+            OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders);
 
             SetDragCursor(mousePosition);
         }
@@ -716,7 +713,7 @@ namespace Avalonia.Controls
         }
 
         //TODO DragEvents
-        private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight)
+        private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders)
         {
             if (handled)
             {
@@ -724,7 +721,7 @@ namespace Avalonia.Controls
             }
 
             //handle entry into reorder mode
-            if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
+            if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null)
             {
                 var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders);
                 if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold)

+ 2 - 2
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@@ -88,10 +88,10 @@ namespace Avalonia.Automation.Peers
 
             if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy)
             {
-                return labeledBy.GetName();
+                result = labeledBy.GetName();
             }
 
-            return null;
+            return result;
         }
 
         protected override AutomationPeer? GetParentCore()

+ 21 - 8
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@@ -15,6 +15,7 @@ namespace Avalonia.Controls.Chrome
     [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
     public class CaptionButtons : TemplatedControl
     {
+        private Button? _restoreButton;
         private IDisposable? _disposables;
 
         /// <summary>
@@ -28,14 +29,23 @@ namespace Avalonia.Controls.Chrome
             {
                 HostWindow = hostWindow;
 
-                _disposables = HostWindow.GetObservable(Window.WindowStateProperty)
-                    .Subscribe(x =>
-                    {
-                        PseudoClasses.Set(":minimized", x == WindowState.Minimized);
-                        PseudoClasses.Set(":normal", x == WindowState.Normal);
-                        PseudoClasses.Set(":maximized", x == WindowState.Maximized);
-                        PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
-                    });
+                _disposables = new CompositeDisposable
+                {
+                    HostWindow.GetObservable(Window.CanResizeProperty)
+                        .Subscribe(x =>
+                        {
+                            if (_restoreButton is not null)
+                                _restoreButton.IsEnabled = x;
+                        }),
+                    HostWindow.GetObservable(Window.WindowStateProperty)
+                        .Subscribe(x =>
+                        {
+                            PseudoClasses.Set(":minimized", x == WindowState.Minimized);
+                            PseudoClasses.Set(":normal", x == WindowState.Normal);
+                            PseudoClasses.Set(":maximized", x == WindowState.Maximized);
+                            PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
+                        }),
+                };
             }
         }
 
@@ -94,6 +104,9 @@ namespace Avalonia.Controls.Chrome
             restoreButton.Click += (sender, e) => OnRestore();
             minimiseButton.Click += (sender, e) => OnMinimize();
             fullScreenButton.Click += (sender, e) => OnToggleFullScreen();
+
+            restoreButton.IsEnabled = HostWindow?.CanResize ?? true;
+            _restoreButton = restoreButton;
         }
     }
 }

+ 13 - 3
src/Avalonia.Controls/Flyouts/Flyout.cs

@@ -13,12 +13,22 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<object> ContentProperty =
             AvaloniaProperty.Register<Flyout, object>(nameof(Content));
 
+        private Classes? _classes;
+
         /// <summary>
         /// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
         /// </summary>
-        public Classes FlyoutPresenterClasses => _classes ??= new Classes();
-
-        private Classes? _classes;
+        public Classes FlyoutPresenterClasses
+        {
+            get => _classes ??= new Classes();
+            set
+            {
+                if (_classes is null)
+                    _classes = value;
+                else if (_classes != value)
+                    _classes.Replace(value);
+            }
+        }
 
         /// <summary>
         /// Defines the <see cref="FlyoutPresenterTheme"/> property.

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

@@ -1,16 +1,9 @@
-using Avalonia.Controls.Primitives;
-
 namespace Avalonia.Controls
 {
     /// <summary>
-    /// Interface for objects that are selectable.
+    /// An interface that is implemented by objects that expose their selection state via a
+    /// boolean <see cref="IsSelected"/> property.
     /// </summary>
-    /// <remarks>
-    /// Controls such as <see cref="SelectingItemsControl"/> use this interface to indicate the
-    /// selected control in a list. If changing the control's <see cref="IsSelected"/> property
-    /// should update the selection in a <see cref="SelectingItemsControl"/> or equivalent, then
-    /// the control should raise the <see cref="SelectingItemsControl.IsSelectedChangedEvent"/>.
-    /// </remarks>
     public interface ISelectable
     {
         /// <summary>
@@ -18,4 +11,4 @@ namespace Avalonia.Controls
         /// </summary>
         bool IsSelected { get; set; }
     }
-}
+}

+ 53 - 7
src/Avalonia.Controls/ItemsControl.cs

@@ -94,7 +94,6 @@ namespace Avalonia.Controls
         private ItemContainerGenerator? _itemContainerGenerator;
         private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
         private IDataTemplate? _displayMemberItemTemplate;
-        private ScrollViewer? _scrollViewer;
         private ItemsPresenter? _itemsPresenter;
 
         /// <summary>
@@ -409,14 +408,35 @@ namespace Avalonia.Controls
                     ic.ItemContainerTheme = ict;
             }
 
-            // This condition is separate because HeaderedItemsControl needs to also run the
-            // ItemsControl preparation.
+            // These conditions are separate because HeaderedItemsControl and
+            // HeaderedSelectingItemsControl also need to run the ItemsControl preparation.
             if (container is HeaderedItemsControl hic)
             {
                 hic.Header = item;
                 hic.HeaderTemplate = itemTemplate;
-                hic.PrepareItemContainer();
+                hic.PrepareItemContainer(this);
             }
+            else if (container is HeaderedSelectingItemsControl hsic)
+            {
+                hsic.Header = item;
+                hsic.HeaderTemplate = itemTemplate;
+                hsic.PrepareItemContainer(this);
+            }
+        }
+
+        /// <summary>
+        /// Called when a container has been fully prepared to display an item.
+        /// </summary>
+        /// <param name="container">The container control.</param>
+        /// <param name="item">The item being displayed.</param>
+        /// <param name="index">The index of the item being displayed.</param>
+        /// <remarks>
+        /// This method will be called when a container has been fully prepared and added to the
+        /// logical and visual trees, but may be called before a layout pass has completed. It is
+        /// called immediately before the <see cref="ContainerPrepared"/> event is raised.
+        /// </remarks>
+        protected internal virtual void ContainerForItemPreparedOverride(Control container, object? item, int index)
+        {
         }
 
         /// <summary>
@@ -436,6 +456,34 @@ namespace Avalonia.Controls
         /// <param name="container">The container element.</param>
         protected internal virtual void ClearContainerForItemOverride(Control container)
         {
+            if (container is HeaderedContentControl hcc)
+            {
+                if (hcc.Content is Control)
+                    hcc.Content = null;
+                if (hcc.Header is Control)
+                    hcc.Header = null;
+            }
+            else if (container is ContentControl cc)
+            {
+                if (cc.Content is Control)
+                    cc.Content = null;
+            }
+            else if (container is ContentPresenter p)
+            {
+                if (p.Content is Control)
+                    p.Content = null;
+            }
+            else if (container is HeaderedItemsControl hic)
+            {
+                if (hic.Header is Control)
+                    hic.Header = null;
+            }
+            else if (container is HeaderedSelectingItemsControl hsic)
+            {
+                if (hsic.Header is Control)
+                    hsic.Header = null;
+            }
+
             // Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at
             // the WPF source it seems that this isn't done there.
         }
@@ -451,7 +499,6 @@ namespace Avalonia.Controls
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
             base.OnApplyTemplate(e);
-            _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
             _itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
         }
 
@@ -622,8 +669,8 @@ namespace Avalonia.Controls
 
         internal void ItemContainerPrepared(Control container, object? item, int index)
         {
+            ContainerForItemPreparedOverride(container, item, index);
             _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
-            _scrollViewer?.RegisterAnchorCandidate(container);
             ContainerPrepared?.Invoke(this, new(container, index));
         }
 
@@ -636,7 +683,6 @@ namespace Avalonia.Controls
 
         internal void ClearItemContainer(Control container)
         {
-            _scrollViewer?.UnregisterAnchorCandidate(container);
             ClearContainerForItemOverride(container);
             ContainerClearing?.Invoke(this, new(container));
         }

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

@@ -1,6 +1,7 @@
 using Avalonia.Automation.Peers;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
+using Avalonia.Controls.Primitives;
 
 namespace Avalonia.Controls
 {
@@ -14,7 +15,7 @@ namespace Avalonia.Controls
         /// Defines the <see cref="IsSelected"/> property.
         /// </summary>
         public static readonly StyledProperty<bool> IsSelectedProperty =
-            AvaloniaProperty.Register<ListBoxItem, bool>(nameof(IsSelected));
+            SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
 
         /// <summary>
         /// Initializes static members of the <see cref="ListBoxItem"/> class.

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

@@ -57,7 +57,7 @@ namespace Avalonia.Controls
         /// Defines the <see cref="IsSelected"/> property.
         /// </summary>
         public static readonly StyledProperty<bool> IsSelectedProperty =
-            ListBoxItem.IsSelectedProperty.AddOwner<MenuItem>();
+            SelectingItemsControl.IsSelectedProperty.AddOwner<MenuItem>();
 
         /// <summary>
         /// Defines the <see cref="IsSubMenuOpen"/> property.

+ 7 - 1
src/Avalonia.Controls/Platform/IInsetsManager.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Media;
 using Avalonia.Metadata;
 
 #nullable enable
@@ -22,7 +23,12 @@ namespace Avalonia.Controls.Platform
         /// Gets the current safe area padding.
         /// </summary>
         Thickness SafeAreaPadding { get; }
-        
+
+        /// <summary>
+        /// Gets or sets the color of the platform's system bars
+        /// </summary>
+        Color? SystemBarColor { get; set; }
+
         /// <summary>
         /// Occurs when safe area for the current window changes.
         /// </summary>

+ 102 - 41
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -6,6 +6,7 @@ using Avalonia.Input;
 using Avalonia.Input.GestureRecognizers;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
+using System.Linq;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -19,44 +20,34 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollContentPresenter, bool> CanHorizontallyScrollProperty =
-            AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>(
-                nameof(CanHorizontallyScroll),
-                o => o.CanHorizontallyScroll,
-                (o, v) => o.CanHorizontallyScroll = v);
+        public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
+            AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanHorizontallyScroll));
 
         /// <summary>
         /// Defines the <see cref="CanVerticallyScroll"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollContentPresenter, bool> CanVerticallyScrollProperty =
-            AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>(
-                nameof(CanVerticallyScroll),
-                o => o.CanVerticallyScroll,
-                (o, v) => o.CanVerticallyScroll = v);
+        public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
+            AvaloniaProperty.Register<ScrollContentPresenter, bool>(nameof(CanVerticallyScroll));
 
         /// <summary>
         /// Defines the <see cref="Extent"/> property.
         /// </summary>
         public static readonly DirectProperty<ScrollContentPresenter, Size> ExtentProperty =
             ScrollViewer.ExtentProperty.AddOwner<ScrollContentPresenter>(
-                o => o.Extent,
-                (o, v) => o.Extent = v);
+                o => o.Extent);
 
         /// <summary>
         /// Defines the <see cref="Offset"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollContentPresenter, Vector> OffsetProperty =
-            ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>(
-                o => o.Offset,
-                (o, v) => o.Offset = v);
+        public static readonly StyledProperty<Vector> OffsetProperty =
+            ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>(new(coerce: ScrollViewer.CoerceOffset));
 
         /// <summary>
         /// Defines the <see cref="Viewport"/> property.
         /// </summary>
         public static readonly DirectProperty<ScrollContentPresenter, Size> ViewportProperty =
             ScrollViewer.ViewportProperty.AddOwner<ScrollContentPresenter>(
-                o => o.Viewport,
-                (o, v) => o.Viewport = v);
+                o => o.Viewport);
 
         /// <summary>
         /// Defines the <see cref="HorizontalSnapPointsType"/> property.
@@ -88,16 +79,13 @@ namespace Avalonia.Controls.Presenters
         public static readonly StyledProperty<bool> IsScrollChainingEnabledProperty =
             ScrollViewer.IsScrollChainingEnabledProperty.AddOwner<ScrollContentPresenter>();
 
-        private bool _canHorizontallyScroll;
-        private bool _canVerticallyScroll;
         private bool _arranging;
         private Size _extent;
-        private Vector _offset;
         private IDisposable? _logicalScrollSubscription;
         private Size _viewport;
         private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
         private Dictionary<int, Vector>? _scrollGestureSnapPoints;
-        private List<Control>? _anchorCandidates;
+        private HashSet<Control>? _anchorCandidates;
         private Control? _anchorElement;
         private Rect _anchorElementBounds;
         private bool _isAnchorElementDirty;
@@ -109,6 +97,8 @@ namespace Avalonia.Controls.Presenters
         private double _verticalSnapPoint;
         private double _verticalSnapPointOffset;
         private double _horizontalSnapPointOffset;
+        private CompositeDisposable? _ownerSubscriptions;
+        private ScrollViewer? _owner;
 
         /// <summary>
         /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@@ -116,7 +106,6 @@ namespace Avalonia.Controls.Presenters
         static ScrollContentPresenter()
         {
             ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
-            ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>((x, e) => x.ChildChanged(e));
         }
 
         /// <summary>
@@ -137,8 +126,8 @@ namespace Avalonia.Controls.Presenters
         /// </summary>
         public bool CanHorizontallyScroll
         {
-            get { return _canHorizontallyScroll; }
-            set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); }
+            get => GetValue(CanHorizontallyScrollProperty);
+            set => SetValue(CanHorizontallyScrollProperty, value);
         }
 
         /// <summary>
@@ -146,8 +135,8 @@ namespace Avalonia.Controls.Presenters
         /// </summary>
         public bool CanVerticallyScroll
         {
-            get { return _canVerticallyScroll; }
-            set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); }
+            get => GetValue(CanVerticallyScrollProperty);
+            set => SetValue(CanVerticallyScrollProperty, value);
         }
 
         /// <summary>
@@ -164,8 +153,8 @@ namespace Avalonia.Controls.Presenters
         /// </summary>
         public Vector Offset
         {
-            get { return _offset; }
-            set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); }
+            get => GetValue(OffsetProperty);
+            set => SetValue(OffsetProperty, value);
         }
 
         /// <summary>
@@ -295,12 +284,60 @@ namespace Avalonia.Controls.Presenters
 
             if (result)
             {
-                Offset = offset;
+                SetCurrentValue(OffsetProperty, offset);
             }
 
             return result;
         }
 
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            AttachToScrollViewer();
+        }
+
+        /// <summary>
+        /// Locates the first <see cref="ScrollViewer"/> ancestor and binds to it. Properties which have been set through other means are not bound.
+        /// </summary>
+        /// <remarks>
+        /// This method is automatically called when the control is attached to a visual tree.
+        /// </remarks>
+        protected internal virtual void AttachToScrollViewer()
+        {
+            var owner = this.FindAncestorOfType<ScrollViewer>();
+
+            if (owner == null)
+            {
+                _owner = null;
+                _ownerSubscriptions?.Dispose();
+                _ownerSubscriptions = null;
+                return;
+            }
+
+            if (owner == _owner)
+            {
+                return;
+            }
+
+            _ownerSubscriptions?.Dispose();
+
+            var subscriptionDisposables = new IDisposable?[]
+            {
+                IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)),
+                IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty, NotDisabled), Data.BindingPriority.Template)),
+                IfUnset(OffsetProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template)),
+                IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)),
+                IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)),
+            }.Where(d => d != null).Cast<IDisposable>().ToArray();
+
+            _owner = owner;
+            _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
+
+            static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled;
+
+            IDisposable? IfUnset<T>(T property, Func<T, IDisposable> func) where T : AvaloniaProperty => IsSet(property) ? null : func(property);
+        }
+
         /// <inheritdoc/>
         void IScrollAnchorProvider.RegisterAnchorCandidate(Control element)
         {
@@ -310,7 +347,7 @@ namespace Avalonia.Controls.Presenters
                     "An anchor control must be a visual descendent of the ScrollContentPresenter.");
             }
 
-            _anchorCandidates ??= new List<Control>();
+            _anchorCandidates ??= new();
             _anchorCandidates.Add(element);
             _isAnchorElementDirty = true;
         }
@@ -410,7 +447,7 @@ namespace Avalonia.Controls.Presenters
                     try
                     {
                         _arranging = true;
-                        Offset = newOffset;
+                        SetCurrentValue(OffsetProperty, newOffset);
                     }
                     finally
                     {
@@ -427,7 +464,6 @@ namespace Avalonia.Controls.Presenters
 
             Viewport = finalSize;
             Extent = Child!.Bounds.Size.Inflate(Child.Margin);
-            Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset);
             _isAnchorElementDirty = true;
 
             return finalSize;
@@ -516,7 +552,7 @@ namespace Avalonia.Controls.Presenters
                 }
 
                 bool offsetChanged = newOffset != Offset;
-                Offset = newOffset;
+                SetCurrentValue(OffsetProperty, newOffset);
 
                 e.Handled = !IsScrollChainingEnabled || offsetChanged;
 
@@ -529,7 +565,7 @@ namespace Avalonia.Controls.Presenters
             _activeLogicalGestureScrolls?.Remove(e.Id);
             _scrollGestureSnapPoints?.Remove(e.Id);
 
-            Offset = SnapOffset(Offset);
+            SetCurrentValue(OffsetProperty, SnapOffset(Offset));
         }
 
         private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
@@ -623,7 +659,7 @@ namespace Avalonia.Controls.Presenters
                 Vector newOffset = SnapOffset(new Vector(x, y));
 
                 bool offsetChanged = newOffset != Offset;
-                Offset = newOffset;
+                SetCurrentValue(OffsetProperty, newOffset);
 
                 e.Handled = !IsScrollChainingEnabled || offsetChanged;
             }
@@ -631,9 +667,14 @@ namespace Avalonia.Controls.Presenters
 
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
-            if (change.Property == OffsetProperty && !_arranging)
+            if (change.Property == OffsetProperty)
             {
-                InvalidateArrange();
+                if (!_arranging)
+                {
+                    InvalidateArrange();
+                }
+
+                _owner?.SetCurrentValue(OffsetProperty, change.GetNewValue<Vector>());
             }
             else if (change.Property == ContentProperty)
             {
@@ -651,11 +692,31 @@ namespace Avalonia.Controls.Presenters
 
                 UpdateSnapPoints();
             }
+            else if (change.Property == ChildProperty)
+            {
+                ChildChanged(change);
+            }
             else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
                 change.Property == VerticalSnapPointsAlignmentProperty)
             {
                 UpdateSnapPoints();
             }
+            else if (change.Property == ExtentProperty)
+            {
+                if (_owner != null)
+                {
+                    _owner.Extent = change.GetNewValue<Size>();
+                }
+                CoerceValue(OffsetProperty);
+            }
+            else if (change.Property == ViewportProperty)
+            {
+                if (_owner != null)
+                {
+                    _owner.Viewport = change.GetNewValue<Size>();
+                }
+                CoerceValue(OffsetProperty);
+            }
 
             base.OnPropertyChanged(change);
         }
@@ -677,7 +738,7 @@ namespace Avalonia.Controls.Presenters
 
             if (e.OldValue != null)
             {
-                Offset = default;
+                SetCurrentValue(OffsetProperty, default);
             }
         }
 
@@ -719,14 +780,14 @@ namespace Avalonia.Controls.Presenters
             if (logicalScroll != scrollable.IsLogicalScrollEnabled)
             {
                 UpdateScrollableSubscription(Child);
-                Offset = default;
+                SetCurrentValue(OffsetProperty, default);
                 InvalidateMeasure();
             }
             else if (scrollable.IsLogicalScrollEnabled)
             {
                 Viewport = scrollable.Viewport;
                 Extent = scrollable.Extent;
-                Offset = scrollable.Offset;
+                SetCurrentValue(OffsetProperty, scrollable.Offset);
             }
         }
 

+ 8 - 8
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives
     public class HeaderedItemsControl : ItemsControl, IContentPresenterHost
     {
         private IDisposable? _itemsBinding;
-        private bool _prepareItemContainerOnAttach;
+        private ItemsControl? _prepareItemContainerOnAttach;
 
         /// <summary>
         /// Defines the <see cref="Header"/> property.
@@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives
         {
             base.OnAttachedToLogicalTree(e);
 
-            if (_prepareItemContainerOnAttach)
+            if (_prepareItemContainerOnAttach is not null)
             {
-                PrepareItemContainer();
-                _prepareItemContainerOnAttach = false;
+                PrepareItemContainer(_prepareItemContainerOnAttach);
+                _prepareItemContainerOnAttach = null;
             }
         }
 
@@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives
             return false;
         }
 
-        internal void PrepareItemContainer()
+        internal void PrepareItemContainer(ItemsControl parent)
         {
             _itemsBinding?.Dispose();
             _itemsBinding = null;
@@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives
 
             if (item is null)
             {
-                _prepareItemContainerOnAttach = false;
+                _prepareItemContainerOnAttach = null;
                 return;
             }
 
-            var headerTemplate = HeaderTemplate;
+            var headerTemplate = HeaderTemplate ?? parent.ItemTemplate;
 
             if (headerTemplate is null)
             {
                 if (((ILogical)this).IsAttachedToLogicalTree)
                     headerTemplate = this.FindDataTemplate(item);
                 else
-                    _prepareItemContainerOnAttach = true;
+                    _prepareItemContainerOnAttach = parent;
             }
 
             if (headerTemplate is ITreeDataTemplate treeTemplate &&

+ 63 - 0
src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@@ -1,5 +1,8 @@
+using System;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls.Primitives
@@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives
     /// </summary>
     public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost
     {
+        private IDisposable? _itemsBinding;
+        private ItemsControl? _prepareItemContainerOnAttach;
+
         /// <summary>
         /// Defines the <see cref="Header"/> property.
         /// </summary>
         public static readonly StyledProperty<object?> HeaderProperty =
             HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
 
+        /// <summary>
+        /// Defines the <see cref="HeaderTemplate"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
+            HeaderedItemsControl.HeaderTemplateProperty.AddOwner<HeaderedSelectingItemsControl>();
+
         /// <summary>
         /// Initializes static members of the <see cref="ContentControl"/> class.
         /// </summary>
@@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives
             set { SetValue(HeaderProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the data template used to display the header content of the control.
+        /// </summary>
+        public IDataTemplate? HeaderTemplate
+        {
+            get => GetValue(HeaderTemplateProperty);
+            set => SetValue(HeaderTemplateProperty, value);
+        }
+
         /// <summary>
         /// Gets the header presenter from the control's template.
         /// </summary>
@@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives
             return RegisterContentPresenter(presenter);
         }
 
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (_prepareItemContainerOnAttach is not null)
+            {
+                PrepareItemContainer(_prepareItemContainerOnAttach);
+                _prepareItemContainerOnAttach = null;
+            }
+        }
+
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>
@@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives
             return false;
         }
 
+        internal void PrepareItemContainer(ItemsControl parent)
+        {
+            _itemsBinding?.Dispose();
+            _itemsBinding = null;
+
+            var item = Header;
+
+            if (item is null)
+            {
+                _prepareItemContainerOnAttach = null;
+                return;
+            }
+
+            var headerTemplate = HeaderTemplate ?? parent.ItemTemplate;
+
+            if (headerTemplate is null)
+            {
+                if (((ILogical)this).IsAttachedToLogicalTree)
+                    headerTemplate = this.FindDataTemplate(item);
+                else
+                    _prepareItemContainerOnAttach = parent;
+            }
+
+            if (headerTemplate is ITreeDataTemplate treeTemplate &&
+                treeTemplate.Match(item) &&
+                treeTemplate.ItemsSelector(item) is { } itemsBinding)
+            {
+                _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null);
+            }
+        }
+
         private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
         {
             if (e.OldValue is ILogical oldChild)

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

@@ -90,7 +90,6 @@ namespace Avalonia.Controls.Primitives
         public void Dispose()
         {
             PlatformImpl?.Dispose();
-            HandleClosed();
         }
 
         private void UpdatePosition()

+ 57 - 108
src/Avalonia.Controls/Primitives/RangeBase.cs

@@ -12,30 +12,22 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Minimum"/> property.
         /// </summary>
-        public static readonly DirectProperty<RangeBase, double> MinimumProperty =
-            AvaloniaProperty.RegisterDirect<RangeBase, double>(
-                nameof(Minimum),
-                o => o.Minimum,
-                (o, v) => o.Minimum = v);
+        public static readonly StyledProperty<double> MinimumProperty =
+            AvaloniaProperty.Register<RangeBase, double>(nameof(Minimum), coerce: CoerceMinimum);
 
         /// <summary>
         /// Defines the <see cref="Maximum"/> property.
         /// </summary>
-        public static readonly DirectProperty<RangeBase, double> MaximumProperty =
-            AvaloniaProperty.RegisterDirect<RangeBase, double>(
-                nameof(Maximum),
-                o => o.Maximum,
-                (o, v) => o.Maximum = v);
+        public static readonly StyledProperty<double> MaximumProperty =
+            AvaloniaProperty.Register<RangeBase, double>(nameof(Maximum), 100, coerce: CoerceMaximum);
 
         /// <summary>
         /// Defines the <see cref="Value"/> property.
         /// </summary>
-        public static readonly DirectProperty<RangeBase, double> ValueProperty =
-            AvaloniaProperty.RegisterDirect<RangeBase, double>(
-                nameof(Value),
-                o => o.Value,
-                (o, v) => o.Value = v,
-                defaultBindingMode: BindingMode.TwoWay);
+        public static readonly StyledProperty<double> ValueProperty =
+            AvaloniaProperty.Register<RangeBase, double>(nameof(Value),
+                defaultBindingMode: BindingMode.TwoWay,
+                coerce: CoerceValue);
 
         /// <summary>
         /// Defines the <see cref="SmallChange"/> property.
@@ -49,44 +41,26 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<double> LargeChangeProperty =
             AvaloniaProperty.Register<RangeBase, double>(nameof(LargeChange), 10);
 
-        private double _minimum;
-        private double _maximum = 100.0;
-        private double _value;
-
         /// <summary>
-        /// Initializes a new instance of the <see cref="RangeBase"/> class.
+        /// Gets or sets the minimum value.
         /// </summary>
-        public RangeBase()
+        public double Minimum
         {
+            get => GetValue(MinimumProperty);
+            set => SetValue(MinimumProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the minimum value.
-        /// </summary>
-        public double Minimum
+        private static double CoerceMinimum(AvaloniaObject sender, double value)
         {
-            get
-            {
-                return _minimum;
-            }
+            return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
+        }
 
-            set
+        private void OnMinimumChanged()
+        {
+            if (IsInitialized)
             {
-                if (!ValidateDouble(value))
-                {
-                    return;
-                }
-
-                if (IsInitialized)
-                {
-                    SetAndRaise(MinimumProperty, ref _minimum, value);
-                    Maximum = ValidateMaximum(Maximum);
-                    Value = ValidateValue(Value);
-                }
-                else
-                {
-                    SetAndRaise(MinimumProperty, ref _minimum, value);
-                }
+                CoerceValue(MaximumProperty);
+                CoerceValue(ValueProperty);
             }
         }
 
@@ -95,28 +69,22 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double Maximum
         {
-            get
-            {
-                return _maximum;
-            }
+            get => GetValue(MaximumProperty);
+            set => SetValue(MaximumProperty, value);
+        }
+
+        private static double CoerceMaximum(AvaloniaObject sender, double value)
+        {
+            return ValidateDouble(value)
+                ? Math.Max(value, sender.GetValue(MinimumProperty))
+                : sender.GetValue(MaximumProperty);
+        }
 
-            set
+        private void OnMaximumChanged()
+        {
+            if (IsInitialized)
             {
-                if (!ValidateDouble(value))
-                {
-                    return;
-                }
-
-                if (IsInitialized)
-                {
-                    value = ValidateMaximum(value);
-                    SetAndRaise(MaximumProperty, ref _maximum, value);
-                    Value = ValidateValue(Value);
-                }
-                else
-                {
-                    SetAndRaise(MaximumProperty, ref _maximum, value);
-                }
+                CoerceValue(ValueProperty);
             }
         }
 
@@ -125,28 +93,15 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double Value
         {
-            get
-            {
-                return _value;
-            }
+            get => GetValue(ValueProperty);
+            set => SetValue(ValueProperty, value);
+        }
 
-            set
-            {
-                if (!ValidateDouble(value))
-                {
-                    return;
-                }
-
-                if (IsInitialized)
-                {
-                    value = ValidateValue(value);
-                    SetAndRaise(ValueProperty, ref _value, value);
-                }
-                else
-                {
-                    SetAndRaise(ValueProperty, ref _value, value);
-                }
-            }
+        private static double CoerceValue(AvaloniaObject sender, double value)
+        {
+            return ValidateDouble(value)
+                ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty))
+                : sender.GetValue(ValueProperty);
         }
 
         public double SmallChange
@@ -165,37 +120,31 @@ namespace Avalonia.Controls.Primitives
         {
             base.OnInitialized();
 
-            Maximum = ValidateMaximum(Maximum);
-            Value = ValidateValue(Value);
+            CoerceValue(MaximumProperty);
+            CoerceValue(ValueProperty);
         }
 
-        /// <summary>
-        /// Checks if the double value is not infinity nor NaN.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        private static bool ValidateDouble(double value)
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
-            return !double.IsInfinity(value) && !double.IsNaN(value);
-        }
+            base.OnPropertyChanged(change);
 
-        /// <summary>
-        /// Validates/coerces the <see cref="Maximum"/> property.
-        /// </summary>
-        /// <param name="value">The value.</param>
-        /// <returns>The coerced value.</returns>
-        private double ValidateMaximum(double value)
-        {
-            return Math.Max(value, Minimum);
+            if (change.Property == MinimumProperty)
+            {
+                OnMinimumChanged();
+            }
+            else if (change.Property == MaximumProperty)
+            {
+                OnMaximumChanged();
+            }
         }
 
         /// <summary>
-        /// Validates/coerces the <see cref="Value"/> property.
+        /// Checks if the double value is not infinity nor NaN.
         /// </summary>
         /// <param name="value">The value.</param>
-        /// <returns>The coerced value.</returns>
-        private double ValidateValue(double value)
+        private static bool ValidateDouble(double value)
         {
-            return MathUtilities.Clamp(value, Minimum, Maximum);
+            return !double.IsInfinity(value) && !double.IsNaN(value);
         }
     }
 }

+ 74 - 5
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -6,6 +6,9 @@ using Avalonia.Layout;
 using Avalonia.Threading;
 using Avalonia.Controls.Metadata;
 using Avalonia.Automation.Peers;
+using Avalonia.VisualTree;
+using Avalonia.Reactive;
+using System.Linq;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -80,6 +83,8 @@ namespace Avalonia.Controls.Primitives
         private Button? _pageDownButton;
         private DispatcherTimer? _timer;
         private bool _isExpanded;
+        private CompositeDisposable? _ownerSubscriptions;
+        private ScrollViewer? _owner;
 
         /// <summary>
         /// Initializes static members of the <see cref="ScrollBar"/> class. 
@@ -88,6 +93,8 @@ namespace Avalonia.Controls.Primitives
         {
             Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
             Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
+
+            FocusableProperty.OverrideMetadata<ScrollBar>(new(false));
         }
 
         /// <summary>
@@ -178,9 +185,62 @@ namespace Avalonia.Controls.Primitives
                 _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
             };
 
-            SetValue(IsVisibleProperty, isVisible);
+            SetCurrentValue(IsVisibleProperty, isVisible);
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            AttachToScrollViewer();
+        }
+
+        /// <summary>
+        /// Locates the first <see cref="ScrollViewer"/> ancestor and binds to its properties. Properties which have been set through other means are not bound.
+        /// </summary>
+        /// <remarks>
+        /// This method is automatically called when the control is attached to a visual tree.
+        /// </remarks>
+        protected internal virtual void AttachToScrollViewer()
+        {
+            var owner = this.FindAncestorOfType<ScrollViewer>();
+
+            if (owner == null)
+            {
+                _owner = null;
+                _ownerSubscriptions?.Dispose();
+                _ownerSubscriptions = null;
+                return;
+            }
+
+            if (owner == _owner)
+            {
+                return;
+            }
+
+            _ownerSubscriptions?.Dispose();
+
+            var visibilitySource = Orientation == Orientation.Horizontal ? ScrollViewer.HorizontalScrollBarVisibilityProperty : ScrollViewer.VerticalScrollBarVisibilityProperty;
+
+            var subscriptionDisposables = new IDisposable?[]
+            {
+                IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty, ExtractOrdinate), BindingPriority.Template)),
+                IfUnset(ValueProperty, p => Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty, ExtractOrdinate), BindingPriority.Template)),
+                IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty, ExtractOrdinate), BindingPriority.Template)),
+                IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)),
+                IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)),
+                IfUnset(LargeChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.LargeChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)),
+                IfUnset(SmallChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.SmallChangeProperty).Select(ExtractOrdinate), BindingPriority.Template))
+            }.Where(d => d != null).Cast<IDisposable>().ToArray();
+
+            _owner = owner;
+            _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
+
+            IDisposable? IfUnset<T>(T property, Func<T, IDisposable> func) where T : AvaloniaProperty => IsSet(property) ? null : func(property);
         }
 
+        private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y;
+        private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height;
+
         protected override void OnKeyDown(KeyEventArgs e)
         {
             if (e.Key == Key.PageUp)
@@ -202,11 +262,20 @@ namespace Avalonia.Controls.Primitives
             if (change.Property == OrientationProperty)
             {
                 UpdatePseudoClasses(change.GetNewValue<Orientation>());
+                if (IsAttachedToVisualTree)
+                {
+                    AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them
+                }
             }
             else if (change.Property == AllowAutoHideProperty)
             {
                 UpdateIsExpandedState();
             }
+            else if (change.Property == ValueProperty)
+            {
+                var value = change.GetNewValue<double>();
+                _owner?.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? _owner.Offset.WithX(value) : _owner.Offset.WithY(value));
+            }
             else
             {
                 if (change.Property == MinimumProperty ||
@@ -373,25 +442,25 @@ namespace Avalonia.Controls.Primitives
 
         private void SmallDecrement()
         {
-            Value = Math.Max(Value - SmallChange, Minimum);
+            SetCurrentValue(ValueProperty, Math.Max(Value - SmallChange, Minimum));
             OnScroll(ScrollEventType.SmallDecrement);
         }
 
         private void SmallIncrement()
         {
-            Value = Math.Min(Value + SmallChange, Maximum);
+            SetCurrentValue(ValueProperty, Math.Min(Value + SmallChange, Maximum));
             OnScroll(ScrollEventType.SmallIncrement);
         }
 
         private void LargeDecrement()
         {
-            Value = Math.Max(Value - LargeChange, Minimum);
+            SetCurrentValue(ValueProperty, Math.Max(Value - LargeChange, Minimum));
             OnScroll(ScrollEventType.LargeDecrement);
         }
 
         private void LargeIncrement()
         {
-            Value = Math.Min(Value + LargeChange, Maximum);
+            SetCurrentValue(ValueProperty, Math.Min(Value + LargeChange, Maximum));
             OnScroll(ScrollEventType.LargeIncrement);
         }
 

+ 75 - 48
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -104,6 +104,14 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
                 nameof(SelectionMode));
 
+        /// <summary>
+        /// Defines the IsSelected attached property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsSelectedProperty =
+            AvaloniaProperty.RegisterAttached<SelectingItemsControl, Control, bool>(
+                "IsSelected",
+                defaultBindingMode: BindingMode.TwoWay);
+
         /// <summary>
         /// Defines the <see cref="IsTextSearchEnabled"/> property.
         /// </summary>
@@ -111,9 +119,8 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false);
 
         /// <summary>
-        /// Event that should be raised by items that implement <see cref="ISelectable"/> to
-        /// notify the parent <see cref="SelectingItemsControl"/> that their selection state
-        /// has changed.
+        /// Event that should be raised by containers when their selection state changes to notify
+        /// the parent <see cref="SelectingItemsControl"/> that their selection state has changed.
         /// </summary>
         public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
             RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>(
@@ -302,20 +309,9 @@ namespace Avalonia.Controls.Primitives
         {
             get
             {
-                if (_updateState?.Selection.HasValue == true)
-                {
-                    return _updateState.Selection.Value;
-                }
-                else
-                {
-                    if (_selection is null)
-                    {
-                        _selection = CreateDefaultSelectionModel();
-                        InitializeSelectionModel(_selection);
-                    }
-
-                    return _selection;
-                }
+                return _updateState?.Selection.HasValue == true ?
+                    _updateState.Selection.Value :
+                    GetOrCreateSelectionModel();
             }
             set
             {
@@ -420,6 +416,21 @@ namespace Avalonia.Controls.Primitives
         /// <param name="item">The item.</param>
         public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item));
 
+        /// <summary>
+        /// Gets the value of the <see cref="IsSelectedProperty"/> on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <returns>The value of the attached property.</returns>
+        public static bool GetIsSelected(Control control) => control.GetValue(IsSelectedProperty);
+
+        /// <summary>
+        /// Gets the value of the <see cref="IsSelectedProperty"/> on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="value">The value of the property.</param>
+        /// <returns>The value of the attached property.</returns>
+        public static void SetIsSelected(Control control, bool value) => control.SetValue(IsSelectedProperty, value);
+
         /// <summary>
         /// Tries to get the container that was the source of an event.
         /// </summary>
@@ -473,20 +484,36 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        /// <inheritdoc />
-        protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
+        protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
+        {
+            // Ensure that the selection model is created at this point so that accessing it in 
+            // ContainerForItemPreparedOverride doesn't cause it to be initialized (which can
+            // make containers become deselected when they're synced with the empty selection
+            // mode).
+            GetOrCreateSelectionModel();
+
+            base.PrepareContainerForItemOverride(container, item, index);
+        }
+
+        protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
         {
-            base.PrepareContainerForItemOverride(element, item, index);
+            base.ContainerForItemPreparedOverride(container, item, index);
 
-            if ((element as ISelectable)?.IsSelected == true)
+            // Once the container has been full prepared and added to the tree, any bindings from
+            // styles or item container themes are guaranteed to be applied. 
+            if (!container.IsSet(IsSelectedProperty))
             {
-                Selection.Select(index);
-                MarkContainerSelected(element, true);
+                // The IsSelected property is not set on the container: update the container
+                // selection based on the current selection as understood by this control.
+                MarkContainerSelected(container, Selection.IsSelected(index));
             }
             else
             {
-                var selected = Selection.IsSelected(index);
-                MarkContainerSelected(element, selected);
+                // The IsSelected property is set on the container: there is a style or item
+                // container theme which has bound the IsSelected property. Update our selection
+                // based on the selection state of the container.
+                var containerIsSelected = GetIsSelected(container);
+                UpdateSelection(index, containerIsSelected, toggleModifier: true);
             }
         }
 
@@ -508,8 +535,7 @@ namespace Avalonia.Controls.Primitives
                 KeyboardNavigation.SetTabOnceActiveElement(panel, null);
             }
 
-            if (element is ISelectable)
-                MarkContainerSelected(element, false);
+            element.ClearValue(IsSelectedProperty);
         }
 
         /// <inheritdoc/>
@@ -874,6 +900,17 @@ namespace Avalonia.Controls.Primitives
             return false;
         }
 
+        private ISelectionModel GetOrCreateSelectionModel()
+        {
+            if (_selection is null)
+            {
+                _selection = CreateDefaultSelectionModel();
+                InitializeSelectionModel(_selection);
+            }
+
+            return _selection;
+        }
+
         private void OnItemsViewSourceChanged(object? sender, EventArgs e)
         {
             if (_selection is not null && _updateState is null)
@@ -1098,11 +1135,14 @@ namespace Avalonia.Controls.Primitives
         {
             if (!_ignoreContainerSelectionChanged &&
                 e.Source is Control control &&
-                e.Source is ISelectable selectable &&
                 control.Parent == this &&
-                IndexFromContainer(control) != -1)
+                IndexFromContainer(control) is var index &&
+                index >= 0)
             {
-                UpdateSelection(control, selectable.IsSelected);
+                if (GetIsSelected(control))
+                    Selection.Select(index);
+                else
+                    Selection.Deselect(index);
             }
 
             if (e.Source != this)
@@ -1112,31 +1152,18 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// Sets the <see cref="IsSelectedProperty"/> on the specified container.
         /// </summary>
         /// <param name="container">The container.</param>
         /// <param name="selected">Whether the control is selected</param>
         /// <returns>The previous selection state.</returns>
-        private bool MarkContainerSelected(Control container, bool selected)
+        private void MarkContainerSelected(Control container, bool selected)
         {
+            _ignoreContainerSelectionChanged = true;
+
             try
             {
-                bool result;
-
-                _ignoreContainerSelectionChanged = true;
-
-                if (container is ISelectable selectable)
-                {
-                    result = selectable.IsSelected;
-                    selectable.IsSelected = selected;
-                }
-                else
-                {
-                    result = container.Classes.Contains(":selected");
-                    ((IPseudoClasses)container.Classes).Set(":selected", selected);
-                }
-
-                return result;
+                container.SetCurrentValue(IsSelectedProperty, selected);
             }
             finally
             {

+ 6 - 3
src/Avalonia.Controls/Primitives/Thumb.cs

@@ -2,6 +2,7 @@ using System;
 using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastPoint.HasValue)
             {
+                var point = e.GetPosition(this.GetVisualParent());
                 var ev = new VectorEventArgs
                 {
                     RoutedEvent = DragDeltaEvent,
-                    Vector = e.GetPosition(this) - _lastPoint.Value,
+                    Vector = point - _lastPoint.Value,
                 };
 
                 RaiseEvent(ev);
+                _lastPoint = point;
             }
         }
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
             e.Handled = true;
-            _lastPoint = e.GetPosition(this);
+            _lastPoint = e.GetPosition(this.GetVisualParent());
 
             var ev = new VectorEventArgs
             {
@@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives
                 var ev = new VectorEventArgs
                 {
                     RoutedEvent = DragCompletedEvent,
-                    Vector = (Vector)e.GetPosition(this),
+                    Vector = (Vector)e.GetPosition(this.GetVisualParent()),
                 };
 
                 RaiseEvent(ev);

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