浏览代码

Merge remote-tracking branch 'origin/master' into fixes/remove-legacy-renderers

Nikita Tsukanov 2 年之前
父节点
当前提交
2a21dbdc7e
共有 45 个文件被更改,包括 1849 次插入232 次删除
  1. 4 1
      samples/ControlCatalog/MainView.xaml
  2. 1 1
      samples/ControlCatalog/MainView.xaml.cs
  3. 23 12
      samples/ControlCatalog/Pages/GesturePage.cs
  4. 222 0
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  5. 68 0
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs
  6. 4 3
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  7. 8 0
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
  8. 4 1
      src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
  9. 5 1
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  10. 28 6
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  11. 4 0
      src/Avalonia.Base/Input/Gestures.cs
  12. 12 0
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  13. 1 1
      src/Avalonia.Base/Media/Brush.cs
  14. 28 9
      src/Avalonia.Base/Media/GlyphRun.cs
  15. 7 4
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  16. 1 1
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  17. 12 8
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  18. 85 69
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  19. 86 81
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  20. 4 8
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  21. 2 1
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  22. 89 2
      src/Avalonia.Controls/ItemsControl.cs
  23. 122 1
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  24. 366 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  25. 50 0
      src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs
  26. 23 0
      src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs
  27. 23 0
      src/Avalonia.Controls/Primitives/SnapPointsType.cs
  28. 81 0
      src/Avalonia.Controls/ScrollViewer.cs
  29. 190 1
      src/Avalonia.Controls/StackPanel.cs
  30. 20 4
      src/Avalonia.Controls/TopLevel.cs
  31. 231 1
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  32. 5 1
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  33. 2 0
      src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml
  34. 4 0
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  35. 6 1
      src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
  36. 2 0
      src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml
  37. 5 1
      src/Avalonia.Themes.Simple/Controls/ListBox.xaml
  38. 5 1
      src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml
  39. 5 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  40. 3 3
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  41. 1 1
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  42. 2 1
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  43. 2 1
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  44. 1 1
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  45. 2 1
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

+ 4 - 1
samples/ControlCatalog/MainView.xaml

@@ -144,9 +144,12 @@
       <TabItem Header="RelativePanel">
       <TabItem Header="RelativePanel">
         <pages:RelativePanelPage />
         <pages:RelativePanelPage />
       </TabItem>
       </TabItem>
-        <TabItem Header="ScrollViewer">
+      <TabItem Header="ScrollViewer">
         <pages:ScrollViewerPage />
         <pages:ScrollViewerPage />
       </TabItem>
       </TabItem>
+      <TabItem Header="ScrollViewer Snapping">
+        <pages:ScrollSnapPage />
+      </TabItem>
       <TabItem Header="Slider">
       <TabItem Header="Slider">
         <pages:SliderPage />
         <pages:SliderPage />
       </TabItem>
       </TabItem>

+ 1 - 1
samples/ControlCatalog/MainView.xaml.cs

@@ -60,7 +60,7 @@ namespace ControlCatalog
             {
             {
                 if (flowDirections.SelectedItem is FlowDirection flowDirection)
                 if (flowDirections.SelectedItem is FlowDirection flowDirection)
                 {
                 {
-                    this.FlowDirection = flowDirection;
+                    TopLevel.GetTopLevel(this).FlowDirection = flowDirection;
                 }
                 }
             };
             };
 
 

+ 23 - 12
samples/ControlCatalog/Pages/GesturePage.cs

@@ -6,6 +6,7 @@ using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml;
 using Avalonia.Rendering.Composition;
 using Avalonia.Rendering.Composition;
+using Avalonia.Utilities;
 
 
 namespace ControlCatalog.Pages
 namespace ControlCatalog.Pages
 {
 {
@@ -53,6 +54,7 @@ namespace ControlCatalog.Pages
                 {
                 {
                     _currentScale = 1;
                     _currentScale = 1;
                     compositionVisual.Scale = new Vector3(1,1,1);
                     compositionVisual.Scale = new Vector3(1,1,1);
+                    compositionVisual.Offset = default;
                     image.InvalidateMeasure();
                     image.InvalidateMeasure();
                 }
                 }
             };
             };
@@ -100,13 +102,19 @@ namespace ControlCatalog.Pages
             {
             {
                 InitComposition(control!);
                 InitComposition(control!);
 
 
-                isZooming = true;
-
                 if(compositionVisual != null)
                 if(compositionVisual != null)
                 {
                 {
                     var scale = _currentScale * (float)e.Scale;
                     var scale = _currentScale * (float)e.Scale;
 
 
+                    if (scale <= 1)
+                    {
+                        scale = 1;
+                        compositionVisual.Offset = default;
+                    }
+
                     compositionVisual.Scale = new(scale, scale, 1);
                     compositionVisual.Scale = new(scale, scale, 1);
+
+                    e.Handled = true;
                 }
                 }
             });
             });
 
 
@@ -114,8 +122,6 @@ namespace ControlCatalog.Pages
             {
             {
                 InitComposition(control!);
                 InitComposition(control!);
 
 
-                isZooming = false;
-
                 if (compositionVisual != null)
                 if (compositionVisual != null)
                 {
                 {
                     _currentScale = compositionVisual.Scale.X;
                     _currentScale = compositionVisual.Scale.X;
@@ -126,11 +132,19 @@ namespace ControlCatalog.Pages
             {
             {
                 InitComposition(control!);
                 InitComposition(control!);
 
 
-                if (compositionVisual != null && !isZooming)
+                if (compositionVisual != null && _currentScale != 1)
                 {
                 {
-                    currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
+                    currentOffset += new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
+
+                    var currentSize = control.Bounds.Size * _currentScale;
+
+                    currentOffset = new Vector3((float)MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
+                        (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
+                        0);
 
 
-                    compositionVisual.Offset = currentOffset;
+                    compositionVisual.Offset = currentOffset * -1;
+
+                    e.Handled = true;
                 }
                 }
             });
             });
         }
         }
@@ -173,6 +187,8 @@ namespace ControlCatalog.Pages
                 if (ballCompositionVisual != null)
                 if (ballCompositionVisual != null)
                 {
                 {
                     ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
                     ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
+
+                    e.Handled = true;
                 }
                 }
             });
             });
 
 
@@ -187,11 +203,6 @@ namespace ControlCatalog.Pages
 
 
             void InitComposition(Control control)
             void InitComposition(Control control)
             {
             {
-                if (ballCompositionVisual != null)
-                {
-                    return;
-                }
-
                 ballCompositionVisual = ElementComposition.GetElementVisual(ball);
                 ballCompositionVisual = ElementComposition.GetElementVisual(ball);
 
 
                 if (ballCompositionVisual != null)
                 if (ballCompositionVisual != null)

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

@@ -0,0 +1,222 @@
+<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, or using the pointer wheel.</TextBlock>
+
+    <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
+      <StackPanel Orientation="Horizontal"
+                  Spacing="4">
+        <StackPanel Orientation="Vertical"
+                    Spacing="4">
+          <TextBlock Text="Snap Point Type" />
+          <ComboBox Items="{Binding AvailableSnapPointsType}"
+                    SelectedItem="{Binding SnapPointsType}" />
+        </StackPanel>
+
+        <StackPanel Orientation="Vertical"
+                    Spacing="4">
+          <TextBlock Text="Snap Point Alignment" />
+          <ComboBox Items="{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>

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

@@ -0,0 +1,68 @@
+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);
+        }
+    }
+}

+ 4 - 3
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@@ -3,12 +3,13 @@
              xmlns:pages="using:ControlCatalog.Pages"
              xmlns:pages="using:ControlCatalog.Pages"
              x:Class="ControlCatalog.Pages.ScrollViewerPage"
              x:Class="ControlCatalog.Pages.ScrollViewerPage"
              x:DataType="pages:ScrollViewerPageViewModel">
              x:DataType="pages:ScrollViewerPageViewModel">
-  <StackPanel Orientation="Vertical" Spacing="4">
-    <TextBlock Classes="h2">Allows for horizontal and vertical content scrolling.</TextBlock>
+  <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, *">
     <Grid ColumnDefinitions="Auto, *">
       <StackPanel Orientation="Vertical" Spacing="4">
       <StackPanel Orientation="Vertical" Spacing="4">
         <ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
         <ToggleSwitch IsChecked="{Binding AllowAutoHide}" Content="Allow auto hide" />
+        <ToggleSwitch IsChecked="{Binding EnableInertia}" Content="Enable Inertia" />
 
 
         <StackPanel Orientation="Vertical" Spacing="4">
         <StackPanel Orientation="Vertical" Spacing="4">
           <TextBlock Text="Horizontal Scroll" />
           <TextBlock Text="Horizontal Scroll" />
@@ -24,6 +25,7 @@
       <ScrollViewer x:Name="ScrollViewer"
       <ScrollViewer x:Name="ScrollViewer"
                     Grid.Column="1"
                     Grid.Column="1"
                     Width="400" Height="400"
                     Width="400" Height="400"
+                    IsScrollInertiaEnabled="{Binding EnableInertia}"
                     AllowAutoHide="{Binding AllowAutoHide}"
                     AllowAutoHide="{Binding AllowAutoHide}"
                     HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
                     HorizontalScrollBarVisibility="{Binding HorizontalScrollVisibility}"
                     VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
                     VerticalScrollBarVisibility="{Binding VerticalScrollVisibility}">
@@ -31,6 +33,5 @@
                Source="/Assets/delicate-arch-896885_640.jpg" />
                Source="/Assets/delicate-arch-896885_640.jpg" />
       </ScrollViewer>
       </ScrollViewer>
     </Grid>
     </Grid>
-
   </StackPanel>
   </StackPanel>
 </UserControl>
 </UserControl>

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

@@ -9,6 +9,7 @@ namespace ControlCatalog.Pages
     public class ScrollViewerPageViewModel : ViewModelBase
     public class ScrollViewerPageViewModel : ViewModelBase
     {
     {
         private bool _allowAutoHide;
         private bool _allowAutoHide;
+        private bool _enableInertia;
         private ScrollBarVisibility _horizontalScrollVisibility;
         private ScrollBarVisibility _horizontalScrollVisibility;
         private ScrollBarVisibility _verticalScrollVisibility;
         private ScrollBarVisibility _verticalScrollVisibility;
 
 
@@ -25,6 +26,7 @@ namespace ControlCatalog.Pages
             HorizontalScrollVisibility = ScrollBarVisibility.Auto;
             HorizontalScrollVisibility = ScrollBarVisibility.Auto;
             VerticalScrollVisibility = ScrollBarVisibility.Auto;
             VerticalScrollVisibility = ScrollBarVisibility.Auto;
             AllowAutoHide = true;
             AllowAutoHide = true;
+            EnableInertia = true;
         }
         }
 
 
         public bool AllowAutoHide
         public bool AllowAutoHide
@@ -33,6 +35,12 @@ namespace ControlCatalog.Pages
             set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
             set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
         }
         }
 
 
+        public bool EnableInertia
+        {
+            get => _enableInertia;
+            set => this.RaiseAndSetIfChanged(ref _enableInertia, value);
+        }
+
         public ScrollBarVisibility HorizontalScrollVisibility
         public ScrollBarVisibility HorizontalScrollVisibility
         {
         {
             get => _horizontalScrollVisibility;
             get => _horizontalScrollVisibility;

+ 4 - 1
src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs

@@ -57,7 +57,10 @@ namespace Avalonia.Input
 
 
                     var scale = distance / _initialDistance;
                     var scale = distance / _initialDistance;
 
 
-                    _target?.RaiseEvent(new PinchEventArgs(scale, _origin));
+                    var pinchEventArgs = new PinchEventArgs(scale, _origin);
+                    _target?.RaiseEvent(pinchEventArgs);
+
+                    e.Handled = pinchEventArgs.Handled;
                 }
                 }
             }
             }
         }
         }

+ 5 - 1
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Diagnostics;
 using Avalonia.Input.GestureRecognizers;
 using Avalonia.Input.GestureRecognizers;
 
 
 namespace Avalonia.Input
 namespace Avalonia.Input
@@ -88,7 +89,10 @@ namespace Avalonia.Input
                 }
                 }
 
 
                 _pullInProgress = true;
                 _pullInProgress = true;
-                _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
+                var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection);
+                _target?.RaiseEvent(pullEventArgs);
+
+                e.Handled = pullEventArgs.Handled;
             }
             }
         }
         }
 
 

+ 28 - 6
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -8,6 +8,10 @@ namespace Avalonia.Input.GestureRecognizers
         : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
         : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
             IGestureRecognizer
             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 _scrolling;
         private bool _scrolling;
         private Point _trackedRootPoint;
         private Point _trackedRootPoint;
         private IPointer? _tracking;
         private IPointer? _tracking;
@@ -23,7 +27,8 @@ namespace Avalonia.Input.GestureRecognizers
         // Movement per second
         // Movement per second
         private Vector _inertia;
         private Vector _inertia;
         private ulong? _lastMoveTimestamp;
         private ulong? _lastMoveTimestamp;
-        
+        private bool _isScrollInertiaEnabled;
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// </summary>
         /// </summary>
@@ -42,6 +47,15 @@ namespace Avalonia.Input.GestureRecognizers
                 o => o.CanVerticallyScroll,
                 o => o.CanVerticallyScroll,
                 (o, v) => o.CanVerticallyScroll = v);
                 (o, v) => o.CanVerticallyScroll = v);
 
 
+        /// <summary>
+        /// Defines the <see cref="IsScrollInertiaEnabled"/> property.
+        /// </summary>
+        public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
+                nameof(IsScrollInertiaEnabled),
+                o => o.IsScrollInertiaEnabled,
+                (o, v) => o.IsScrollInertiaEnabled = v);
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="ScrollStartDistance"/> property.
         /// Defines the <see cref="ScrollStartDistance"/> property.
         /// </summary>
         /// </summary>
@@ -68,6 +82,15 @@ namespace Avalonia.Input.GestureRecognizers
             get => _canVerticallyScroll;
             get => _canVerticallyScroll;
             set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
             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 => _isScrollInertiaEnabled;
+            set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
+        }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating the distance the pointer moves before scrolling is started
         /// Gets or sets a value indicating the distance the pointer moves before scrolling is started
@@ -97,9 +120,6 @@ namespace Avalonia.Input.GestureRecognizers
             }
             }
         }
         }
         
         
-        // Pixels per second speed that is considered to be the stop of inertial scroll
-        private const double InertialScrollSpeedEnd = 5;
-        
         public void PointerMoved(PointerEventArgs e)
         public void PointerMoved(PointerEventArgs e)
         {
         {
             if (e.Pointer == _tracking)
             if (e.Pointer == _tracking)
@@ -168,7 +188,8 @@ namespace Avalonia.Input.GestureRecognizers
                 if (_inertia == default
                 if (_inertia == default
                     || e.Timestamp == 0
                     || e.Timestamp == 0
                     || _lastMoveTimestamp == 0
                     || _lastMoveTimestamp == 0
-                    || e.Timestamp - _lastMoveTimestamp > 200)
+                    || e.Timestamp - _lastMoveTimestamp > 200
+                    || !IsScrollInertiaEnabled)
                     EndGesture();
                     EndGesture();
                 else
                 else
                 {
                 {
@@ -176,6 +197,7 @@ namespace Avalonia.Input.GestureRecognizers
                     var savedGestureId = _gestureId;
                     var savedGestureId = _gestureId;
                     var st = Stopwatch.StartNew();
                     var st = Stopwatch.StartNew();
                     var lastTime = TimeSpan.Zero;
                     var lastTime = TimeSpan.Zero;
+                    _target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia));
                     DispatcherTimer.Run(() =>
                     DispatcherTimer.Run(() =>
                     {
                     {
                         // Another gesture has started, finish the current one
                         // Another gesture has started, finish the current one
@@ -187,7 +209,7 @@ namespace Avalonia.Input.GestureRecognizers
                         var elapsedSinceLastTick = st.Elapsed - lastTime;
                         var elapsedSinceLastTick = st.Elapsed - lastTime;
                         lastTime = st.Elapsed;
                         lastTime = st.Elapsed;
 
 
-                        var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
+                        var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds);
                         var distance = speed * elapsedSinceLastTick.TotalSeconds;
                         var distance = speed * elapsedSinceLastTick.TotalSeconds;
                         var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
                         var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
                         _target!.RaiseEvent(scrollGestureEventArgs);
                         _target!.RaiseEvent(scrollGestureEventArgs);

+ 4 - 0
src/Avalonia.Base/Input/Gestures.cs

@@ -45,6 +45,10 @@ namespace Avalonia.Input
             RoutedEvent.Register<ScrollGestureEventArgs>(
             RoutedEvent.Register<ScrollGestureEventArgs>(
                 "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
                 "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
 
 
+        public static readonly RoutedEvent<ScrollGestureInertiaStartingEventArgs> ScrollGestureInertiaStartingEvent =
+            RoutedEvent.Register<ScrollGestureInertiaStartingEventArgs>(
+                "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures));
+
         public static readonly RoutedEvent<ScrollGestureEndedEventArgs> ScrollGestureEndedEvent =
         public static readonly RoutedEvent<ScrollGestureEndedEventArgs> ScrollGestureEndedEvent =
             RoutedEvent.Register<ScrollGestureEndedEventArgs>(
             RoutedEvent.Register<ScrollGestureEndedEventArgs>(
                 "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
                 "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));

+ 12 - 0
src/Avalonia.Base/Input/ScrollGestureEventArgs.cs

@@ -30,4 +30,16 @@ namespace Avalonia.Input
             Id = id;
             Id = id;
         }
         }
     }
     }
+
+    public class ScrollGestureInertiaStartingEventArgs : RoutedEventArgs
+    {
+        public int Id { get; }
+        public Vector Inertia { get; }
+
+        internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent)
+        {
+            Id = id;
+            Inertia = inertia;
+        }
+    }
 }
 }

+ 1 - 1
src/Avalonia.Base/Media/Brush.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Media
     /// Describes how an area is painted.
     /// Describes how an area is painted.
     /// </summary>
     /// </summary>
     [TypeConverter(typeof(BrushConverter))]
     [TypeConverter(typeof(BrushConverter))]
-    public abstract class Brush : Animatable
+    public abstract class Brush : Animatable, IBrush
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Opacity"/> property.
         /// Defines the <see cref="Opacity"/> property.

+ 28 - 9
src/Avalonia.Base/Media/GlyphRun.cs

@@ -13,14 +13,22 @@ namespace Avalonia.Media
     /// </summary>
     /// </summary>
     public sealed class GlyphRun : IDisposable
     public sealed class GlyphRun : IDisposable
     {
     {
+        private readonly static IPlatformRenderInterface s_renderInterface;
+
         private IRef<IGlyphRunImpl>? _platformImpl;
         private IRef<IGlyphRunImpl>? _platformImpl;
         private double _fontRenderingEmSize;
         private double _fontRenderingEmSize;
         private int _biDiLevel;
         private int _biDiLevel;
         private GlyphRunMetrics? _glyphRunMetrics;
         private GlyphRunMetrics? _glyphRunMetrics;
         private ReadOnlyMemory<char> _characters;
         private ReadOnlyMemory<char> _characters;
         private IReadOnlyList<GlyphInfo> _glyphInfos;
         private IReadOnlyList<GlyphInfo> _glyphInfos;
+        private Point? _baselineOrigin;
         private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
         private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
 
 
+        static GlyphRun()
+        {
+            s_renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
+        }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// </summary>
         /// </summary>
@@ -28,15 +36,17 @@ namespace Avalonia.Media
         /// <param name="fontRenderingEmSize">The rendering em size.</param>
         /// <param name="fontRenderingEmSize">The rendering em size.</param>
         /// <param name="characters">The characters.</param>
         /// <param name="characters">The characters.</param>
         /// <param name="glyphIndices">The glyph indices.</param>
         /// <param name="glyphIndices">The glyph indices.</param>
+        /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         public GlyphRun(
         public GlyphRun(
             IGlyphTypeface glyphTypeface,
             IGlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
             double fontRenderingEmSize,
             ReadOnlyMemory<char> characters,
             ReadOnlyMemory<char> characters,
             IReadOnlyList<ushort> glyphIndices,
             IReadOnlyList<ushort> glyphIndices,
+            Point? baselineOrigin = null,
             int biDiLevel = 0)
             int biDiLevel = 0)
             : this(glyphTypeface, fontRenderingEmSize, characters,
             : this(glyphTypeface, fontRenderingEmSize, characters,
-                CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel)
+                CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), baselineOrigin, biDiLevel)
         {
         {
             _hasOneCharPerCluster = true;
             _hasOneCharPerCluster = true;
         }
         }
@@ -48,12 +58,14 @@ namespace Avalonia.Media
         /// <param name="fontRenderingEmSize">The rendering em size.</param>
         /// <param name="fontRenderingEmSize">The rendering em size.</param>
         /// <param name="characters">The characters.</param>
         /// <param name="characters">The characters.</param>
         /// <param name="glyphInfos">The list of glyphs used.</param>
         /// <param name="glyphInfos">The list of glyphs used.</param>
+        /// <param name="baselineOrigin">The baseline origin of the run.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         /// <param name="biDiLevel">The bidi level.</param>
         public GlyphRun(
         public GlyphRun(
             IGlyphTypeface glyphTypeface,
             IGlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
             double fontRenderingEmSize,
             ReadOnlyMemory<char> characters,
             ReadOnlyMemory<char> characters,
             IReadOnlyList<GlyphInfo> glyphInfos,
             IReadOnlyList<GlyphInfo> glyphInfos,
+            Point? baselineOrigin = null,
             int biDiLevel = 0)
             int biDiLevel = 0)
         {
         {
             GlyphTypeface = glyphTypeface;
             GlyphTypeface = glyphTypeface;
@@ -64,6 +76,8 @@ namespace Avalonia.Media
 
 
             _glyphInfos = glyphInfos;
             _glyphInfos = glyphInfos;
 
 
+            _baselineOrigin = baselineOrigin;
+
             _biDiLevel = biDiLevel;
             _biDiLevel = biDiLevel;
         }
         }
 
 
@@ -72,6 +86,7 @@ namespace Avalonia.Media
             _glyphInfos = Array.Empty<GlyphInfo>();
             _glyphInfos = Array.Empty<GlyphInfo>();
             GlyphTypeface = Typeface.Default.GlyphTypeface;
             GlyphTypeface = Typeface.Default.GlyphTypeface;
             _platformImpl = platformImpl;
             _platformImpl = platformImpl;
+            _baselineOrigin = platformImpl.Item.BaselineOrigin;
         }
         }
 
 
         private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
         private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
@@ -147,9 +162,13 @@ namespace Avalonia.Media
             => _glyphRunMetrics ??= CreateGlyphRunMetrics();
             => _glyphRunMetrics ??= CreateGlyphRunMetrics();
 
 
         /// <summary>
         /// <summary>
-        ///     Gets the baseline origin of the<see cref="GlyphRun"/>.
+        ///     Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
         /// </summary>
         /// </summary>
-        public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin;
+        public Point BaselineOrigin
+        {
+            get => _baselineOrigin ?? default;
+            set => Set(ref _baselineOrigin, value);
+        }
 
 
         /// <summary>
         /// <summary>
         ///     Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
         ///     Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
@@ -204,9 +223,7 @@ namespace Avalonia.Media
         /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
         /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
         public Geometry BuildGeometry()
         public Geometry BuildGeometry()
         {
         {
-            var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
-
-            var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
+            var geometryImpl = s_renderInterface.BuildGlyphRunGeometry(this);
 
 
             return new PlatformGeometry(geometryImpl);
             return new PlatformGeometry(geometryImpl);
         }
         }
@@ -802,9 +819,11 @@ namespace Avalonia.Media
 
 
         private IRef<IGlyphRunImpl> CreateGlyphRunImpl()
         private IRef<IGlyphRunImpl> CreateGlyphRunImpl()
         {
         {
-            var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
-
-            var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
+            var platformImpl = s_renderInterface.CreateGlyphRun(
+                GlyphTypeface, 
+                FontRenderingEmSize, 
+                GlyphInfos, 
+                _baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale));
 
 
             _platformImpl = RefCountable.Create(platformImpl);
             _platformImpl = RefCountable.Create(platformImpl);
 
 

+ 7 - 4
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@@ -93,16 +93,19 @@ namespace Avalonia.Media.TextFormatting
             [Conditional("DEBUG")]
             [Conditional("DEBUG")]
             public void VerifyAllReturned()
             public void VerifyAllReturned()
             {
             {
-                if (_pendingReturnCount > 0)
+                var pendingReturnCount = _pendingReturnCount;
+                _pendingReturnCount = 0;
+
+                if (pendingReturnCount > 0)
                 {
                 {
                     throw new InvalidOperationException(
                     throw new InvalidOperationException(
-                        $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!");
+                        $"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!");
                 }
                 }
 
 
-                if (_pendingReturnCount < 0)
+                if (pendingReturnCount < 0)
                 {
                 {
                     throw new InvalidOperationException(
                     throw new InvalidOperationException(
-                        $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!");
+                        $"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!");
                 }
                 }
             }
             }
         }
         }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@@ -185,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
                 ShapedBuffer.FontRenderingEmSize,
                 ShapedBuffer.FontRenderingEmSize,
                 Text,
                 Text,
                 ShapedBuffer,
                 ShapedBuffer,
-                BidiLevel);
+                biDiLevel: BidiLevel);
         }
         }
 
 
         public void Dispose()
         public void Dispose()

+ 12 - 8
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -113,14 +113,18 @@ namespace Avalonia.Media.TextFormatting
 
 
             var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
             var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
 
 
-            var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
-            preSplitRuns.CopyTo(collapsedRuns);
-            collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
-
-            objectPool.TextRunLists.Return(ref preSplitRuns);
-            objectPool.TextRunLists.Return(ref postSplitRuns);
-
-            return collapsedRuns;
+            try
+            {
+                var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
+                preSplitRuns.CopyTo(collapsedRuns);
+                collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
+                return collapsedRuns;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref preSplitRuns);
+                objectPool.TextRunLists.Return(ref postSplitRuns);
+            }
         }
         }
     }
     }
 }
 }

+ 85 - 69
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -32,58 +32,64 @@ namespace Avalonia.Media.TextFormatting
             var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
             var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
                 out var textEndOfLine, out var textSourceLength);
                 out var textEndOfLine, out var textSourceLength);
 
 
-            RentedList<TextRun>? shapedTextRuns;
+            RentedList<TextRun>? shapedTextRuns = null;
 
 
-            if (previousLineBreak?.RemainingRuns is { } remainingRuns)
+            try
             {
             {
-                resolvedFlowDirection = previousLineBreak.FlowDirection;
-                textRuns = remainingRuns;
-                nextLineBreak = previousLineBreak;
-                shapedTextRuns = null;
-            }
-            else
-            {
-                shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
-                textRuns = shapedTextRuns;
-
-                if (nextLineBreak == null && textEndOfLine != null)
+                if (previousLineBreak?.RemainingRuns is { } remainingRuns)
                 {
                 {
-                    nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                    resolvedFlowDirection = previousLineBreak.FlowDirection;
+                    textRuns = remainingRuns;
+                    nextLineBreak = previousLineBreak;
+                    shapedTextRuns = null;
                 }
                 }
-            }
+                else
+                {
+                    shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
+                        out resolvedFlowDirection);
+                    textRuns = shapedTextRuns;
 
 
-            TextLineImpl textLine;
+                    if (nextLineBreak == null && textEndOfLine != null)
+                    {
+                        nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                    }
+                }
 
 
-            switch (textWrapping)
-            {
-                case TextWrapping.NoWrap:
+                TextLineImpl textLine;
+
+                switch (textWrapping)
                 {
                 {
-                    // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
-                    // which already uses an array: ToArray() won't ever be called in this case
-                    var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
+                    case TextWrapping.NoWrap:
+                    {
+                        // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
+                        // which already uses an array: ToArray() won't ever be called in this case
+                        var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
 
 
-                    textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
-                        paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
+                        textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
+                            paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
 
-                    textLine.FinalizeLine();
+                        textLine.FinalizeLine();
 
 
-                    break;
-                }
-                case TextWrapping.WrapWithOverflow:
-                case TextWrapping.Wrap:
-                {
-                    textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
-                        paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
-                    break;
+                        break;
+                    }
+                    case TextWrapping.WrapWithOverflow:
+                    case TextWrapping.Wrap:
+                    {
+                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
+                            paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
+                        break;
+                    }
+                    default:
+                        throw new ArgumentOutOfRangeException(nameof(textWrapping));
                 }
                 }
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(textWrapping));
-            }
-
-            objectPool.TextRunLists.Return(ref shapedTextRuns);
-            objectPool.TextRunLists.Return(ref fetchedRuns);
 
 
-            return textLine;
+                return textLine;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref shapedTextRuns);
+                objectPool.TextRunLists.Return(ref fetchedRuns);
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -224,23 +230,26 @@ namespace Avalonia.Media.TextFormatting
                 (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
                 (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
 
 
             var processedRuns = objectPool.TextRunLists.Rent();
             var processedRuns = objectPool.TextRunLists.Rent();
+            var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
 
 
-            CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
+            try
+            {
+                CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
 
 
-            bidiData.Reset();
-            bidiAlgorithm.Reset();
+                bidiData.Reset();
+                bidiAlgorithm.Reset();
 
 
-            var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
-            var textShaper = TextShaper.Current;
 
 
-            for (var index = 0; index < processedRuns.Count; index++)
-            {
-                var currentRun = processedRuns[index];
+                var textShaper = TextShaper.Current;
 
 
-                switch (currentRun)
+                for (var index = 0; index < processedRuns.Count; index++)
                 {
                 {
-                    case UnshapedTextRun shapeableRun:
+                    var currentRun = processedRuns[index];
+
+                    switch (currentRun)
                     {
                     {
+                        case UnshapedTextRun shapeableRun:
+                        {
                             groupedRuns.Clear();
                             groupedRuns.Clear();
                             groupedRuns.Add(shapeableRun);
                             groupedRuns.Add(shapeableRun);
 
 
@@ -277,17 +286,20 @@ namespace Avalonia.Media.TextFormatting
 
 
                             break;
                             break;
                         }
                         }
-                    default:
+                        default:
                         {
                         {
                             shapedRuns.Add(currentRun);
                             shapedRuns.Add(currentRun);
 
 
                             break;
                             break;
                         }
                         }
+                    }
                 }
                 }
             }
             }
-
-            objectPool.TextRunLists.Return(ref processedRuns);
-            objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
+            finally
+            {
+                objectPool.TextRunLists.Return(ref processedRuns);
+                objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
+            }
 
 
             return shapedRuns;
             return shapedRuns;
         }
         }
@@ -805,25 +817,29 @@ namespace Avalonia.Media.TextFormatting
 
 
             var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
             var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
 
 
-            var textLineBreak = postSplitRuns?.Count > 0 ?
-                new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
-                null;
-
-            if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+            try
             {
             {
-                textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
-            }
+                var textLineBreak = postSplitRuns?.Count > 0 ?
+                    new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
+                    null;
 
 
-            var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
-                paragraphWidth, paragraphProperties, resolvedFlowDirection,
-                textLineBreak);
-
-            textLine.FinalizeLine();
+                if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+                {
+                    textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
+                }
 
 
-            objectPool.TextRunLists.Return(ref preSplitRuns);
-            objectPool.TextRunLists.Return(ref postSplitRuns);
+                var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
+                    paragraphWidth, paragraphProperties, resolvedFlowDirection,
+                    textLineBreak);
 
 
-            return textLine;
+                textLine.FinalizeLine();
+                return textLine;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref preSplitRuns);
+                objectPool.TextRunLists.Return(ref postSplitRuns);
+            }
         }
         }
 
 
         private struct TextRunEnumerator
         private struct TextRunEnumerator

+ 86 - 81
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -441,128 +441,133 @@ namespace Avalonia.Media.TextFormatting
 
 
             var textLines = objectPool.TextLines.Rent();
             var textLines = objectPool.TextLines.Rent();
 
 
-            double left = double.PositiveInfinity, width = 0.0, height = 0.0;
-
-            _textSourceLength = 0;
+            try
+            {
+                double left = double.PositiveInfinity, width = 0.0, height = 0.0;
 
 
-            TextLine? previousLine = null;
+                _textSourceLength = 0;
 
 
-            var textFormatter = TextFormatter.Current;
+                TextLine? previousLine = null;
 
 
-            while (true)
-            {
-                var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
-                    previousLine?.TextLineBreak);
+                var textFormatter = TextFormatter.Current;
 
 
-                if (textLine.Length == 0)
+                while (true)
                 {
                 {
-                    if (previousLine != null && previousLine.NewLineLength > 0)
+                    var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
+                        _paragraphProperties, previousLine?.TextLineBreak);
+
+                    if (textLine.Length == 0)
                     {
                     {
-                        var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
-                            _paragraphProperties, fontManager);
+                        if (previousLine != null && previousLine.NewLineLength > 0)
+                        {
+                            var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
+                                _paragraphProperties, fontManager);
 
 
-                        textLines.Add(emptyTextLine);
+                            textLines.Add(emptyTextLine);
 
 
-                        UpdateBounds(emptyTextLine, ref left, ref width, ref height);
-                    }
+                            UpdateBounds(emptyTextLine, ref left, ref width, ref height);
+                        }
 
 
-                    break;
-                }
+                        break;
+                    }
 
 
-                _textSourceLength += textLine.Length;
+                    _textSourceLength += textLine.Length;
 
 
-                //Fulfill max height constraint
-                if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
-                {
-                    if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
+                    //Fulfill max height constraint
+                    if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
+                        && height + textLine.Height > MaxHeight)
                     {
                     {
-                        var collapsedLine =
-                            previousLine.Collapse(GetCollapsingProperties(MaxWidth));
+                        if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
+                        {
+                            var collapsedLine =
+                                previousLine.Collapse(GetCollapsingProperties(MaxWidth));
 
 
-                        textLines[textLines.Count - 1] = collapsedLine;
-                    }
+                            textLines[textLines.Count - 1] = collapsedLine;
+                        }
 
 
-                    break;
-                }
+                        break;
+                    }
 
 
-                var hasOverflowed = textLine.HasOverflowed;
+                    var hasOverflowed = textLine.HasOverflowed;
 
 
-                if (hasOverflowed && _textTrimming != TextTrimming.None)
-                {
-                    textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
-                }
+                    if (hasOverflowed && _textTrimming != TextTrimming.None)
+                    {
+                        textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
+                    }
 
 
-                textLines.Add(textLine);
+                    textLines.Add(textLine);
 
 
-                UpdateBounds(textLine, ref left, ref width, ref height);
+                    UpdateBounds(textLine, ref left, ref width, ref height);
 
 
-                previousLine = textLine;
+                    previousLine = textLine;
 
 
-                //Fulfill max lines constraint
-                if (MaxLines > 0 && textLines.Count >= MaxLines)
-                {
-                    if(textLine.TextLineBreak?.RemainingRuns is not null)
+                    //Fulfill max lines constraint
+                    if (MaxLines > 0 && textLines.Count >= MaxLines)
                     {
                     {
-                        textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
+                        if (textLine.TextLineBreak?.RemainingRuns is not null)
+                        {
+                            textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
+                        }
+
+                        break;
                     }
                     }
 
 
-                    break;
+                    if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                    {
+                        break;
+                    }
                 }
                 }
 
 
-                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                //Make sure the TextLayout always contains at least on empty line
+                if (textLines.Count == 0)
                 {
                 {
-                    break;
-                }
-            }
+                    var textLine =
+                        TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
 
 
-            //Make sure the TextLayout always contains at least on empty line
-            if (textLines.Count == 0)
-            {
-                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
-
-                textLines.Add(textLine);
-
-                UpdateBounds(textLine, ref left, ref width, ref height);
-            }
+                    textLines.Add(textLine);
 
 
-            Bounds = new Rect(left, 0, width, height);
+                    UpdateBounds(textLine, ref left, ref width, ref height);
+                }
 
 
-            if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
-            {
-                var whitespaceWidth = 0d;
+                Bounds = new Rect(left, 0, width, height);
 
 
-                for (var i = 0; i < textLines.Count; i++)
+                if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
                 {
                 {
-                    var line = textLines[i];
-                    var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
+                    var whitespaceWidth = 0d;
 
 
-                    if (lineWhitespaceWidth > whitespaceWidth)
+                    for (var i = 0; i < textLines.Count; i++)
                     {
                     {
-                        whitespaceWidth = lineWhitespaceWidth;
-                    }
-                }
+                        var line = textLines[i];
+                        var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
 
 
-                var justificationWidth = width - whitespaceWidth;
+                        if (lineWhitespaceWidth > whitespaceWidth)
+                        {
+                            whitespaceWidth = lineWhitespaceWidth;
+                        }
+                    }
 
 
-                if (justificationWidth > 0)
-                {
-                    var justificationProperties = new InterWordJustification(justificationWidth);
+                    var justificationWidth = width - whitespaceWidth;
 
 
-                    for (var i = 0; i < textLines.Count - 1; i++)
+                    if (justificationWidth > 0)
                     {
                     {
-                        var line = textLines[i];
+                        var justificationProperties = new InterWordJustification(justificationWidth);
 
 
-                        line.Justify(justificationProperties);
+                        for (var i = 0; i < textLines.Count - 1; i++)
+                        {
+                            var line = textLines[i];
+
+                            line.Justify(justificationProperties);
+                        }
                     }
                     }
                 }
                 }
-            }
 
 
-            var result = textLines.ToArray();
-
-            objectPool.TextLines.Return(ref textLines);
-            objectPool.VerifyAllReturned();
-
-            return result;
+                return textLines.ToArray();
+            }
+            finally
+            {
+                objectPool.TextLines.Return(ref textLines);
+                objectPool.VerifyAllReturned();
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 4 - 8
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -86,7 +86,6 @@ namespace Avalonia.Media.TextFormatting
 
 
                                     RentedList<TextRun>? rentedPreSplitRuns = null;
                                     RentedList<TextRun>? rentedPreSplitRuns = null;
                                     RentedList<TextRun>? rentedPostSplitRuns = null;
                                     RentedList<TextRun>? rentedPostSplitRuns = null;
-                                    TextRun[]? results;
 
 
                                     try
                                     try
                                     {
                                     {
@@ -113,9 +112,7 @@ namespace Avalonia.Media.TextFormatting
 
 
                                         if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
                                         if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
                                         {
                                         {
-                                            results = collapsedRuns.ToArray();
-                                            objectPool.TextRunLists.Return(ref collapsedRuns);
-                                            return results;
+                                            return collapsedRuns.ToArray();
                                         }
                                         }
 
 
                                         var availableSuffixWidth = availableWidth;
                                         var availableSuffixWidth = availableWidth;
@@ -157,16 +154,15 @@ namespace Avalonia.Media.TextFormatting
                                                 }
                                                 }
                                             }
                                             }
                                         }
                                         }
+
+                                        return collapsedRuns.ToArray();
                                     }
                                     }
                                     finally
                                     finally
                                     {
                                     {
                                         objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
                                         objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
                                         objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
                                         objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
+                                        objectPool.TextRunLists.Return(ref collapsedRuns);
                                     }
                                     }
-
-                                    results = collapsedRuns.ToArray();
-                                    objectPool.TextRunLists.Return(ref collapsedRuns);
-                                    return results;
                                 }
                                 }
 
 
                                 return new TextRun[] { shapedSymbol };
                                 return new TextRun[] { shapedSymbol };

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

@@ -168,8 +168,9 @@ namespace Avalonia.Platform
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <param name="glyphTypeface">The glyph typeface.</param>
         /// <param name="fontRenderingEmSize">The font rendering em size.</param>
         /// <param name="fontRenderingEmSize">The font rendering em size.</param>
         /// <param name="glyphInfos">The list of glyphs.</param>
         /// <param name="glyphInfos">The list of glyphs.</param>
+        /// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
         /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
         /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
-        IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos);
+        IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
 
 
         /// <summary>
         /// <summary>
         /// Creates a backend-specific object using a low-level API graphics context
         /// Creates a backend-specific object using a low-level API graphics context

+ 89 - 2
src/Avalonia.Controls/ItemsControl.cs

@@ -12,6 +12,8 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
 using Avalonia.Styling;
 using Avalonia.Styling;
@@ -23,7 +25,7 @@ namespace Avalonia.Controls
     /// Displays a collection of items.
     /// Displays a collection of items.
     /// </summary>
     /// </summary>
     [PseudoClasses(":empty", ":singleitem")]
     [PseudoClasses(":empty", ":singleitem")]
-    public class ItemsControl : TemplatedControl, IChildIndexProvider
+    public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo
     {
     {
         /// <summary>
         /// <summary>
         /// The default value for the <see cref="ItemsPanel"/> property.
         /// The default value for the <see cref="ItemsPanel"/> property.
@@ -72,6 +74,18 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
         public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
             AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
             AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
+        
+        /// <summary>
+        /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
         /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
@@ -91,6 +105,8 @@ namespace Avalonia.Controls
         private IDataTemplate? _displayMemberItemTemplate;
         private IDataTemplate? _displayMemberItemTemplate;
         private Tuple<int, Control>? _containerBeingPrepared;
         private Tuple<int, Control>? _containerBeingPrepared;
         private ScrollViewer? _scrollViewer;
         private ScrollViewer? _scrollViewer;
+        private ItemsPresenter? _itemsPresenter;
+        private IScrollSnapPointsInfo? _scrolSnapPointInfo;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemsControl"/> class.
         /// Initializes a new instance of the <see cref="ItemsControl"/> class.
@@ -203,6 +219,63 @@ namespace Avalonia.Controls
             remove => _childIndexChanged -= value;
             remove => _childIndexChanged -= value;
         }
         }
 
 
+
+        public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
+        {
+            add
+            {
+                if (_itemsPresenter != null)
+                {
+                    _itemsPresenter.HorizontalSnapPointsChanged += value;
+                }
+            }
+
+            remove
+            {
+                if (_itemsPresenter != null)
+                {
+                    _itemsPresenter.HorizontalSnapPointsChanged -= value;
+                }
+            }
+        }
+
+        public event EventHandler<RoutedEventArgs> VerticalSnapPointsChanged
+        {
+            add
+            {
+                if (_itemsPresenter != null)
+                {
+                    _itemsPresenter.VerticalSnapPointsChanged += value;
+                }
+            }
+
+            remove
+            {
+                if (_itemsPresenter != null)
+                {
+                    _itemsPresenter.VerticalSnapPointsChanged -= value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
+        /// </summary>
+        public bool AreHorizontalSnapPointsRegular
+        {
+            get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
+            set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
+        /// </summary>
+        public bool AreVerticalSnapPointsRegular
+        {
+            get { return GetValue(AreVerticalSnapPointsRegularProperty); }
+            set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+        }
+
         /// <summary>
         /// <summary>
         /// Returns the container for the item at the specified index.
         /// Returns the container for the item at the specified index.
         /// </summary>
         /// </summary>
@@ -255,7 +328,6 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
         public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
 
 
-
         /// <summary>
         /// <summary>
         /// Creates or a container that can be used to display an item.
         /// Creates or a container that can be used to display an item.
         /// </summary>
         /// </summary>
@@ -355,6 +427,9 @@ namespace Avalonia.Controls
         {
         {
             base.OnApplyTemplate(e);
             base.OnApplyTemplate(e);
             _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
             _scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
+            _itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
+
+            _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -671,5 +746,17 @@ namespace Avalonia.Controls
             count = ItemsView.Count;
             count = ItemsView.Count;
             return true;
             return true;
         }
         }
+
+        public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
+        {
+            return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List<double>();
+        }
+
+        public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
+        {
+            offset = 0;
+
+            return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0;
+        }
     }
     }
 }
 }

+ 122 - 1
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -3,13 +3,15 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
 
 
 namespace Avalonia.Controls.Presenters
 namespace Avalonia.Controls.Presenters
 {
 {
     /// <summary>
     /// <summary>
     /// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
     /// Presents items inside an <see cref="Avalonia.Controls.ItemsControl"/>.
     /// </summary>
     /// </summary>
-    public class ItemsPresenter : Control, ILogicalScrollable
+    public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="ItemsPanel"/> property.
         /// Defines the <see cref="ItemsPanel"/> property.
@@ -19,8 +21,37 @@ namespace Avalonia.Controls.Presenters
 
 
         private PanelContainerGenerator? _generator;
         private PanelContainerGenerator? _generator;
         private ILogicalScrollable? _logicalScrollable;
         private ILogicalScrollable? _logicalScrollable;
+        private IScrollSnapPointsInfo? _scrollSnapPointsInfo;
         private EventHandler? _scrollInvalidated;
         private EventHandler? _scrollInvalidated;
 
 
+        /// <summary>
+        /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreHorizontalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<ItemsControl, bool>(nameof(AreVerticalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
+            RoutedEvent.Register<StackPanel, RoutedEventArgs>(
+                nameof(HorizontalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
+            RoutedEvent.Register<StackPanel, RoutedEventArgs>(
+                nameof(VerticalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
         static ItemsPresenter()
         static ItemsPresenter()
         {
         {
             KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
             KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
@@ -83,12 +114,48 @@ namespace Avalonia.Controls.Presenters
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Occurs when the measurements for horizontal snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
+        {
+            add => AddHandler(HorizontalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs when the measurements for vertical snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
+        {
+            add => AddHandler(VerticalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
+        }
+
         bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;
         bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;
         Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
         Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default;
         Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
         Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default;
         Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
         Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
         Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
         Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
 
 
+        /// <summary>
+        /// Gets or sets whether the horizontal snap points for the <see cref="ItemsControl"/> are equidistant from each other.
+        /// </summary>
+        public bool AreHorizontalSnapPointsRegular
+        {
+            get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
+            set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets whether the vertical snap points for the <see cref="ItemsControl"/> are equidistant from each other.
+        /// </summary>
+        public bool AreVerticalSnapPointsRegular
+        {
+            get { return GetValue(AreVerticalSnapPointsRegularProperty); }
+            set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+        }
+
         public override sealed void ApplyTemplate()
         public override sealed void ApplyTemplate()
         {
         {
             if (Panel is null && ItemsControl is not null)
             if (Panel is null && ItemsControl is not null)
@@ -100,14 +167,36 @@ namespace Avalonia.Controls.Presenters
 
 
                 Panel = ItemsPanel.Build();
                 Panel = ItemsPanel.Build();
                 Panel.SetValue(TemplatedParentProperty, TemplatedParent);
                 Panel.SetValue(TemplatedParentProperty, TemplatedParent);
+                _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo;
                 LogicalChildren.Add(Panel);
                 LogicalChildren.Add(Panel);
                 VisualChildren.Add(Panel);
                 VisualChildren.Add(Panel);
 
 
+                if (_scrollSnapPointsInfo != null)
+                {
+                    _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
+                    _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
+                }
+
                 if (Panel is VirtualizingPanel v)
                 if (Panel is VirtualizingPanel v)
                     v.Attach(ItemsControl);
                     v.Attach(ItemsControl);
                 else
                 else
                     CreateSimplePanelGenerator();
                     CreateSimplePanelGenerator();
 
 
+                if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
+                {
+                    scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) =>
+                    {
+                        e.RoutedEvent = VerticalSnapPointsChangedEvent;
+                        RaiseEvent(e);
+                    };
+
+                    scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) =>
+                    {
+                        e.RoutedEvent = HorizontalSnapPointsChangedEvent;
+                        RaiseEvent(e);
+                    };
+                }
+
                 _logicalScrollable = Panel as ILogicalScrollable;
                 _logicalScrollable = Panel as ILogicalScrollable;
 
 
                 if (_logicalScrollable is not null)
                 if (_logicalScrollable is not null)
@@ -151,6 +240,16 @@ namespace Avalonia.Controls.Presenters
                 ResetState();
                 ResetState();
                 InvalidateMeasure();
                 InvalidateMeasure();
             }
             }
+            else if(change.Property == AreHorizontalSnapPointsRegularProperty)
+            {
+                if (_scrollSnapPointsInfo != null)
+                    _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular;
+            }
+            else if (change.Property == AreVerticalSnapPointsRegularProperty)
+            {
+                if (_scrollSnapPointsInfo != null)
+                    _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular;
+            }
         }
         }
 
 
         internal void Refresh()
         internal void Refresh()
@@ -204,5 +303,27 @@ namespace Avalonia.Controls.Presenters
         }
         }
 
 
         private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
         private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e);
+
+        public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
+        {
+            if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
+            {
+                return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment);
+            }
+
+            return new List<double>();
+        }
+
+        public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
+        {
+            if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo)
+            {
+                return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset);
+            }
+
+            offset = 0;
+
+            return 0;
+        }
     }
     }
 }
 }

+ 366 - 2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using Avalonia.Reactive;
 using Avalonia.Reactive;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
@@ -14,6 +15,7 @@ namespace Avalonia.Controls.Presenters
     public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
     public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
     {
     {
         private const double EdgeDetectionTolerance = 0.1;
         private const double EdgeDetectionTolerance = 0.1;
+        private const int ProximityPoints = 10;
 
 
         /// <summary>
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
@@ -57,6 +59,30 @@ namespace Avalonia.Controls.Presenters
                 o => o.Viewport,
                 o => o.Viewport,
                 (o, v) => o.Viewport = v);
                 (o, v) => o.Viewport = v);
 
 
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsType"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
+            ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsType"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
+           ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner<ScrollContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsAlignment> HorizontalSnapPointsAlignmentProperty =
+            ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsAlignment> VerticalSnapPointsAlignmentProperty =
+            ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner<ScrollContentPresenter>();
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="IsScrollChainingEnabled"/> property.
         /// Defines the <see cref="IsScrollChainingEnabled"/> property.
         /// </summary>
         /// </summary>
@@ -71,10 +97,19 @@ namespace Avalonia.Controls.Presenters
         private IDisposable? _logicalScrollSubscription;
         private IDisposable? _logicalScrollSubscription;
         private Size _viewport;
         private Size _viewport;
         private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
         private Dictionary<int, Vector>? _activeLogicalGestureScrolls;
+        private Dictionary<int, Vector>? _scrollGestureSnapPoints;
         private List<Control>? _anchorCandidates;
         private List<Control>? _anchorCandidates;
         private Control? _anchorElement;
         private Control? _anchorElement;
         private Rect _anchorElementBounds;
         private Rect _anchorElementBounds;
         private bool _isAnchorElementDirty;
         private bool _isAnchorElementDirty;
+        private bool _areVerticalSnapPointsRegular;
+        private bool _areHorizontalSnapPointsRegular;
+        private IReadOnlyList<double>? _horizontalSnapPoints;
+        private double _horizontalSnapPoint;
+        private IReadOnlyList<double>? _verticalSnapPoints;
+        private double _verticalSnapPoint;
+        private double _verticalSnapPointOffset;
+        private double _horizontalSnapPointOffset;
 
 
         /// <summary>
         /// <summary>
         /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
         /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@@ -93,6 +128,7 @@ namespace Avalonia.Controls.Presenters
             AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
             AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
             AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
             AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
             AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
             AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
+            AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded);
 
 
             this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
             this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
         }
         }
@@ -142,6 +178,42 @@ namespace Avalonia.Controls.Presenters
             private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
             private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
+        /// </summary>
+        public SnapPointsType HorizontalSnapPointsType
+        {
+            get => GetValue(HorizontalSnapPointsTypeProperty);
+            set => SetValue(HorizontalSnapPointsTypeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
+        /// </summary>
+        public SnapPointsType VerticalSnapPointsType
+        {
+            get => GetValue(VerticalSnapPointsTypeProperty);
+            set => SetValue(VerticalSnapPointsTypeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
+        /// </summary>
+        public SnapPointsAlignment HorizontalSnapPointsAlignment
+        {
+            get => GetValue(HorizontalSnapPointsAlignmentProperty);
+            set => SetValue(HorizontalSnapPointsAlignmentProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
+        /// </summary>
+        public SnapPointsAlignment VerticalSnapPointsAlignment
+        {
+            get => GetValue(VerticalSnapPointsAlignmentProperty); 
+            set => SetValue(VerticalSnapPointsAlignmentProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         ///  Gets or sets if scroll chaining is enabled. The default value is true.
         ///  Gets or sets if scroll chaining is enabled. The default value is true.
         /// </summary>
         /// </summary>
@@ -424,6 +496,25 @@ namespace Avalonia.Controls.Presenters
                 }
                 }
 
 
                 Vector newOffset = new Vector(x, y);
                 Vector newOffset = new Vector(x, y);
+
+                if (_scrollGestureSnapPoints?.TryGetValue(e.Id, out var snapPoint) == true)
+                {
+                    double xOffset = x;
+                    double yOffset = y;
+
+                    if (HorizontalSnapPointsType != SnapPointsType.None)
+                    {
+                        xOffset = delta.X < 0 ? Math.Max(snapPoint.X, newOffset.X) : Math.Min(snapPoint.X, newOffset.X);
+                    }
+
+                    if (VerticalSnapPointsType != SnapPointsType.None)
+                    {
+                        yOffset = delta.Y < 0 ? Math.Max(snapPoint.Y, newOffset.Y) : Math.Min(snapPoint.Y, newOffset.Y);
+                    }
+
+                    newOffset = new Vector(xOffset, yOffset);
+                }
+
                 bool offsetChanged = newOffset != Offset;
                 bool offsetChanged = newOffset != Offset;
                 Offset = newOffset;
                 Offset = newOffset;
 
 
@@ -434,7 +525,65 @@ namespace Avalonia.Controls.Presenters
         }
         }
 
 
         private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e)
         private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e)
-            => _activeLogicalGestureScrolls?.Remove(e.Id);
+        {
+            _activeLogicalGestureScrolls?.Remove(e.Id);
+            _scrollGestureSnapPoints?.Remove(e.Id);
+
+            Offset = SnapOffset(Offset);
+        }
+
+        private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
+        {
+            if (Content is not IScrollSnapPointsInfo)
+                return;
+
+            if (_scrollGestureSnapPoints == null)
+                _scrollGestureSnapPoints = new Dictionary<int, Vector>();
+
+            var offset = Offset;
+
+            if (HorizontalSnapPointsType != SnapPointsType.None && VerticalSnapPointsType != SnapPointsType.None)
+            {
+                return;
+            }
+
+            double xDistance = 0;
+            double yDistance = 0;
+
+            if (HorizontalSnapPointsType != SnapPointsType.None)
+            {
+                xDistance = HorizontalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.X) : 0;
+            }
+
+            if (VerticalSnapPointsType != SnapPointsType.None)
+            {
+                yDistance = VerticalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.Y) : 0;
+            }
+
+            offset = new Vector(offset.X + xDistance, offset.Y + yDistance);
+
+            System.Diagnostics.Debug.WriteLine($"{offset}");
+
+            _scrollGestureSnapPoints.Add(e.Id, SnapOffset(offset));
+
+            double GetDistance(double speed)
+            {
+                var time = Math.Log(ScrollGestureRecognizer.InertialScrollSpeedEnd / Math.Abs(speed)) / Math.Log(ScrollGestureRecognizer.InertialResistance);
+
+                double timeElapsed = 0, distance = 0, step = 0;
+
+                while (timeElapsed <= time)
+                {
+                    double s = speed * Math.Pow(ScrollGestureRecognizer.InertialResistance, timeElapsed);
+                    distance += (s * step);
+
+                    timeElapsed += 0.016f;
+                    step = 0.016f;
+                }
+
+                return distance;
+            }
+        }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
@@ -458,6 +607,30 @@ namespace Avalonia.Controls.Presenters
                 if (Extent.Height > Viewport.Height)
                 if (Extent.Height > Viewport.Height)
                 {
                 {
                     double height = isLogical ? scrollable!.ScrollSize.Height : 50;
                     double height = isLogical ? scrollable!.ScrollSize.Height : 50;
+                    if(VerticalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
+                    {
+                        if(_areVerticalSnapPointsRegular)
+                        {
+                            height = _verticalSnapPoint;
+                        }
+                        else if(_verticalSnapPoints != null)
+                        {
+                            double yOffset = Offset.Y;
+                            switch (VerticalSnapPointsAlignment)
+                            {
+                                case SnapPointsAlignment.Center:
+                                    yOffset += Viewport.Height / 2;
+                                    break;
+                                case SnapPointsAlignment.Far:
+                                    yOffset += Viewport.Height;
+                                    break;
+                            }
+
+                            var snapPoint = FindNearestSnapPoint(_verticalSnapPoints, yOffset, out var lowerSnapPoint);
+
+                            height = snapPoint - lowerSnapPoint;
+                        }
+                    }
                     y += -delta.Y * height;
                     y += -delta.Y * height;
                     y = Math.Max(y, 0);
                     y = Math.Max(y, 0);
                     y = Math.Min(y, Extent.Height - Viewport.Height);
                     y = Math.Min(y, Extent.Height - Viewport.Height);
@@ -466,12 +639,37 @@ namespace Avalonia.Controls.Presenters
                 if (Extent.Width > Viewport.Width)
                 if (Extent.Width > Viewport.Width)
                 {
                 {
                     double width = isLogical ? scrollable!.ScrollSize.Width : 50;
                     double width = isLogical ? scrollable!.ScrollSize.Width : 50;
+                    if (HorizontalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
+                    {
+                        if (_areHorizontalSnapPointsRegular)
+                        {
+                            width = _horizontalSnapPoint;
+                        }
+                        else if(_horizontalSnapPoints != null)
+                        {
+                            double xOffset = Offset.X;
+                            switch (VerticalSnapPointsAlignment)
+                            {
+                                case SnapPointsAlignment.Center:
+                                    xOffset += Viewport.Width / 2;
+                                    break;
+                                case SnapPointsAlignment.Far:
+                                    xOffset += Viewport.Width;
+                                    break;
+                            }
+
+                            var snapPoint = FindNearestSnapPoint(_horizontalSnapPoints, xOffset, out var lowerSnapPoint);
+
+                            width = snapPoint - lowerSnapPoint;
+                        }
+                    }
                     x += -delta.X * width;
                     x += -delta.X * width;
                     x = Math.Max(x, 0);
                     x = Math.Max(x, 0);
                     x = Math.Min(x, Extent.Width - Viewport.Width);
                     x = Math.Min(x, Extent.Width - Viewport.Width);
                 }
                 }
 
 
-                Vector newOffset = new Vector(x, y);
+                Vector newOffset = SnapOffset(new Vector(x, y));
+
                 bool offsetChanged = newOffset != Offset;
                 bool offsetChanged = newOffset != Offset;
                 Offset = newOffset;
                 Offset = newOffset;
 
 
@@ -485,10 +683,36 @@ namespace Avalonia.Controls.Presenters
             {
             {
                 InvalidateArrange();
                 InvalidateArrange();
             }
             }
+            else if (change.Property == ContentProperty)
+            {
+                if (change.OldValue is IScrollSnapPointsInfo oldSnapPointsInfo)
+                {
+                    oldSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
+                    oldSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
+                }
+
+                if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo)
+                {
+                    scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
+                    scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
+                }
+
+                UpdateSnapPoints();
+            }
+            else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
+                change.Property == VerticalSnapPointsAlignmentProperty)
+            {
+                UpdateSnapPoints();
+            }
 
 
             base.OnPropertyChanged(change);
             base.OnPropertyChanged(change);
         }
         }
 
 
+        private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e)
+        {
+            UpdateSnapPoints();
+        }
+
         private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e)
         private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e)
         {
         {
             if (e.TargetObject is not null)
             if (e.TargetObject is not null)
@@ -635,5 +859,145 @@ namespace Avalonia.Controls.Presenters
             bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
             bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
             return p.HasValue;
             return p.HasValue;
         }
         }
+
+        private void UpdateSnapPoints()
+        {
+            if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo)
+            {
+                _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular;
+                _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular;
+
+                if (!_areVerticalSnapPointsRegular)
+                {
+                    _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment);
+                }
+                else
+                {
+                    _verticalSnapPoints = new List<double>();
+                    _verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset);
+
+                }
+
+                if (!_areHorizontalSnapPointsRegular)
+                {
+                    _horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment);
+                }
+                else
+                {
+                    _horizontalSnapPoints = new List<double>();
+                    _horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _horizontalSnapPointOffset);
+                }
+            }
+            else
+            {
+                _horizontalSnapPoints = new List<double>();
+                _verticalSnapPoints = new List<double>();
+            }
+        }
+
+        private Vector SnapOffset(Vector offset)
+        {
+            if(Content is not IScrollSnapPointsInfo)
+                return offset;
+
+            var diff = GetAlignedDiff();
+
+            if (VerticalSnapPointsType != SnapPointsType.None)
+            {
+                offset = new Vector(offset.X, offset.Y + diff.Y);
+                double nearestSnapPoint = offset.Y;
+
+                if (_areVerticalSnapPointsRegular)
+                {
+                    var minSnapPoint = (int)(offset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset;
+                    var maxSnapPoint = minSnapPoint + _verticalSnapPoint;
+                    var midPoint = (minSnapPoint + maxSnapPoint) / 2;
+
+                    nearestSnapPoint = offset.Y < midPoint ? minSnapPoint : maxSnapPoint;
+                }
+                else if (_verticalSnapPoints != null && _verticalSnapPoints.Count > 0)
+                {
+                    var higherSnapPoint = FindNearestSnapPoint(_verticalSnapPoints, offset.Y, out var lowerSnapPoint);
+                    var midPoint = (lowerSnapPoint + higherSnapPoint) / 2;
+
+                    nearestSnapPoint = offset.Y < midPoint ? lowerSnapPoint : higherSnapPoint;
+                }
+
+                offset = new Vector(offset.X, nearestSnapPoint - diff.Y);
+            }
+
+            if (HorizontalSnapPointsType != SnapPointsType.None)
+            {
+                offset = new Vector(offset.X + diff.X, offset.Y);
+                double nearestSnapPoint = offset.X;
+
+                if (_areHorizontalSnapPointsRegular)
+                {
+                    var minSnapPoint = (int)(offset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset;
+                    var maxSnapPoint = minSnapPoint + _horizontalSnapPoint;
+                    var midPoint = (minSnapPoint + maxSnapPoint) / 2;
+
+                    nearestSnapPoint = offset.X < midPoint ? minSnapPoint : maxSnapPoint;
+                }
+                else if (_horizontalSnapPoints != null && _horizontalSnapPoints.Count > 0)
+                {
+                    var higherSnapPoint = FindNearestSnapPoint(_horizontalSnapPoints, offset.X, out var lowerSnapPoint);
+                    var midPoint = (lowerSnapPoint + higherSnapPoint) / 2;
+
+                    nearestSnapPoint = offset.X < midPoint ? lowerSnapPoint : higherSnapPoint;
+                }
+
+                offset = new Vector(nearestSnapPoint - diff.X, offset.Y);
+
+            }
+
+            Vector GetAlignedDiff()
+            {
+                var vector = offset;
+
+                switch (VerticalSnapPointsAlignment)
+                {
+                    case SnapPointsAlignment.Center:
+                        vector += new Vector(0, Viewport.Height / 2);
+                        break;
+                    case SnapPointsAlignment.Far:
+                        vector += new Vector(0, Viewport.Height);
+                        break;
+                }
+
+                switch (HorizontalSnapPointsAlignment)
+                {
+                    case SnapPointsAlignment.Center:
+                        vector += new Vector(Viewport.Width / 2, 0);
+                        break;
+                    case SnapPointsAlignment.Far:
+                        vector += new Vector(Viewport.Width, 0);
+                        break;
+                }                
+
+                return vector - offset;
+            }
+
+            return offset;
+        }
+
+        private static double FindNearestSnapPoint(IReadOnlyList<double> snapPoints, double value, out double lowerSnapPoint)
+        {
+            var point = snapPoints.BinarySearch(value, Comparer<double>.Default);
+
+            if (point < 0)
+            {
+                point = ~point;
+
+                lowerSnapPoint = snapPoints[Math.Max(0, point - 1)];
+            }
+            else
+            {
+                lowerSnapPoint = snapPoints[point];
+
+                point += 1;
+            }
+            return snapPoints[Math.Min(point, snapPoints.Count - 1)];
+        }
     }
     }
 }
 }

+ 50 - 0
src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Describes snap point behavior for objects that contain and present items.
+    /// </summary>
+    public interface IScrollSnapPointsInfo
+    {
+        /// <summary>
+        /// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other.
+        /// </summary>
+        bool AreHorizontalSnapPointsRegular { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other.
+        /// </summary>
+        bool AreVerticalSnapPointsRegular { get; set; }
+
+        /// <summary>
+        /// Returns the set of distances between irregular snap points for a specified orientation and alignment.
+        /// </summary>
+        /// <param name="orientation">The orientation for the desired snap point set.</param>
+        /// <param name="snapPointsAlignment">The alignment to use when applying the snap points.</param>
+        /// <returns>The read-only collection of snap point distances. Returns an empty collection when no snap points are present.</returns>
+        IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment);
+
+        /// <summary>
+        /// Gets the distance between regular snap points for a specified orientation and alignment.
+        /// </summary>
+        /// <param name="orientation">The orientation for the desired snap point set.</param>
+        /// <param name="snapPointsAlignment">The alignment to use when applying the snap points.</param>
+        /// <param name="offset">Out parameter. The offset of the first snap point.</param>
+        /// <returns>The distance between the equidistant snap points. Returns 0 when no snap points are present.</returns>
+        double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset);
+
+        /// <summary>
+        /// Occurs when the measurements for horizontal snap points change.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged;
+
+        /// <summary>
+        /// Occurs when the measurements for vertical snap points change.
+        /// </summary>
+        event EventHandler<RoutedEventArgs> VerticalSnapPointsChanged;
+    }
+}

+ 23 - 0
src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs

@@ -0,0 +1,23 @@
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Specify options for snap point alignment relative to an edge. Which edge depends on the orientation of the object where the alignment is applied
+    /// </summary>
+    public enum SnapPointsAlignment
+    {
+        /// <summary>
+        /// Use snap points grouped closer to the orientation edge.
+        /// </summary>
+        Near,
+
+        /// <summary>
+        /// Use snap points that are centered in the orientation.
+        /// </summary>
+        Center,
+
+        /// <summary>
+        /// Use snap points grouped farther from the orientation edge.
+        /// </summary>
+        Far
+    }
+}

+ 23 - 0
src/Avalonia.Controls/Primitives/SnapPointsType.cs

@@ -0,0 +1,23 @@
+namespace Avalonia.Controls.Primitives
+{
+    /// <summary>
+    /// Specify how panning snap points are processed for gesture input.
+    /// </summary>
+    public enum SnapPointsType
+    {
+        /// <summary>
+        /// No snapping behavior.
+        /// </summary>
+        None,
+
+        /// <summary>
+        /// Content always stops at the snap point closest to where inertia would naturally stop along the direction of inertia.
+        /// </summary>
+        Mandatory,
+
+        /// <summary>
+        /// Content always stops at the snap point closest to the release point along the direction of inertia.
+        /// </summary>
+        MandatorySingle
+    }
+}

+ 81 - 0
src/Avalonia.Controls/ScrollViewer.cs

@@ -151,6 +151,34 @@ namespace Avalonia.Controls
                 o => o.VerticalScrollBarValue,
                 o => o.VerticalScrollBarValue,
                 (o, v) => o.VerticalScrollBarValue = v);
                 (o, v) => o.VerticalScrollBarValue = v);
 
 
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsType"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsType> HorizontalSnapPointsTypeProperty =
+            AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
+                nameof(HorizontalSnapPointsType));
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsType"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SnapPointsType> VerticalSnapPointsTypeProperty =
+            AvaloniaProperty.Register<ScrollViewer, SnapPointsType>(
+                nameof(VerticalSnapPointsType));
+
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsAlignment"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<SnapPointsAlignment> HorizontalSnapPointsAlignmentProperty =
+            AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>(
+                nameof(HorizontalSnapPointsAlignment));
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsAlignment"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<SnapPointsAlignment> VerticalSnapPointsAlignmentProperty =
+            AvaloniaProperty.RegisterAttached<ScrollViewer, Control, SnapPointsAlignment>(
+                nameof(VerticalSnapPointsAlignment));
+
         /// <summary>
         /// <summary>
         /// Defines the VerticalScrollBarViewportSize property.
         /// Defines the VerticalScrollBarViewportSize property.
         /// </summary>
         /// </summary>
@@ -193,6 +221,14 @@ namespace Avalonia.Controls
                 nameof(IsScrollChainingEnabled),
                 nameof(IsScrollChainingEnabled),
                 defaultValue: true);
                 defaultValue: true);
 
 
+        /// <summary>
+        /// Defines the <see cref="IsScrollInertiaEnabled"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsScrollInertiaEnabledProperty =
+            AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(
+                nameof(IsScrollInertiaEnabled),
+                defaultValue: true);
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="ScrollChanged"/> event.
         /// Defines the <see cref="ScrollChanged"/> event.
         /// </summary>
         /// </summary>
@@ -421,6 +457,42 @@ namespace Avalonia.Controls
             private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value);
             private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis.
+        /// </summary>
+        public SnapPointsType HorizontalSnapPointsType
+        {
+            get => GetValue(HorizontalSnapPointsTypeProperty);
+            set => SetValue(HorizontalSnapPointsTypeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis.
+        /// </summary>
+        public SnapPointsType VerticalSnapPointsType
+        {
+            get => GetValue(VerticalSnapPointsTypeProperty);
+            set => SetValue(VerticalSnapPointsTypeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport.
+        /// </summary>
+        public SnapPointsAlignment HorizontalSnapPointsAlignment
+        {
+            get => GetValue(HorizontalSnapPointsAlignmentProperty); 
+            set => SetValue(HorizontalSnapPointsAlignmentProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport.
+        /// </summary>
+        public SnapPointsAlignment VerticalSnapPointsAlignment
+        {
+            get => GetValue(VerticalSnapPointsAlignmentProperty); 
+            set => SetValue(VerticalSnapPointsAlignmentProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it.
         /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it.
         /// </summary>
         /// </summary>
@@ -444,6 +516,15 @@ namespace Avalonia.Controls
             set => SetValue(IsScrollChainingEnabledProperty, value);
             set => SetValue(IsScrollChainingEnabledProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets whether scroll gestures should include inertia in their behavior and value.
+        /// </summary>
+        public bool IsScrollInertiaEnabled
+        {
+            get => GetValue(IsScrollInertiaEnabledProperty);
+            set => SetValue(IsScrollInertiaEnabledProperty, value);
+        }
+
         /// <summary>
         /// <summary>
         /// Scrolls the content up one line.
         /// Scrolls the content up one line.
         /// </summary>
         /// </summary>

+ 190 - 1
src/Avalonia.Controls/StackPanel.cs

@@ -4,7 +4,11 @@
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 
 using System;
 using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Layout;
 
 
 namespace Avalonia.Controls
 namespace Avalonia.Controls
@@ -12,7 +16,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// <summary>
     /// A panel which lays out its children horizontally or vertically.
     /// A panel which lays out its children horizontally or vertically.
     /// </summary>
     /// </summary>
-    public class StackPanel : Panel, INavigableContainer
+    public class StackPanel : Panel, INavigableContainer, IScrollSnapPointsInfo
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Spacing"/> property.
         /// Defines the <see cref="Spacing"/> property.
@@ -26,6 +30,34 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Orientation> OrientationProperty =
         public static readonly StyledProperty<Orientation> OrientationProperty =
             StackLayout.OrientationProperty.AddOwner<StackPanel>();
             StackLayout.OrientationProperty.AddOwner<StackPanel>();
 
 
+        /// <summary>
+        /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<StackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<StackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
+            RoutedEvent.Register<StackPanel, RoutedEventArgs>(
+                nameof(HorizontalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
+            RoutedEvent.Register<StackPanel, RoutedEventArgs>(
+                nameof(VerticalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
         /// <summary>
         /// <summary>
         /// Initializes static members of the <see cref="StackPanel"/> class.
         /// Initializes static members of the <see cref="StackPanel"/> class.
         /// </summary>
         /// </summary>
@@ -53,6 +85,42 @@ namespace Avalonia.Controls
             set { SetValue(OrientationProperty, value); }
             set { SetValue(OrientationProperty, value); }
         }
         }
 
 
+        /// <summary>
+        /// Occurs when the measurements for horizontal snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
+        {
+            add => AddHandler(HorizontalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs when the measurements for vertical snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
+        {
+            add => AddHandler(VerticalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
+        }
+
+        /// <summary>
+        /// Gets or sets whether the horizontal snap points for the <see cref="StackPanel"/> are equidistant from each other.
+        /// </summary>
+        public bool AreHorizontalSnapPointsRegular
+        {
+            get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
+            set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets whether the vertical snap points for the <see cref="StackPanel"/> are equidistant from each other.
+        /// </summary>
+        public bool AreVerticalSnapPointsRegular
+        {
+            get { return GetValue(AreVerticalSnapPointsRegularProperty); }
+            set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the next control in the specified direction.
         /// Gets the next control in the specified direction.
         /// </summary>
         /// </summary>
@@ -274,6 +342,8 @@ namespace Avalonia.Controls
                 ArrangeChild(child, rcChild, finalSize, Orientation);
                 ArrangeChild(child, rcChild, finalSize, Orientation);
             }
             }
 
 
+            RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
+
             return finalSize;
             return finalSize;
         }
         }
 
 
@@ -285,5 +355,124 @@ namespace Avalonia.Controls
         {
         {
             child.Arrange(rect);
             child.Arrange(rect);
         }
         }
+
+        /// <inheritdoc/>
+        public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
+        {
+            var snapPoints = new List<double>();
+
+            switch (orientation)
+            {
+                case Orientation.Horizontal:
+                    if (AreHorizontalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    if (Orientation == Orientation.Horizontal)
+                    {
+                        foreach(var child in VisualChildren)
+                        {
+                            double snapPoint = 0;
+
+                            switch (snapPointsAlignment)
+                            {
+                                case SnapPointsAlignment.Near:
+                                    snapPoint = child.Bounds.Left;
+                                    break;
+                                case SnapPointsAlignment.Center:
+                                    snapPoint = child.Bounds.Center.X;
+                                    break;
+                                case SnapPointsAlignment.Far:
+                                    snapPoint = child.Bounds.Right;
+                                    break;
+                            }
+
+                            snapPoints.Add(snapPoint);
+                        }
+                    }
+                    break;
+                case Orientation.Vertical:
+                    if (AreVerticalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    if (Orientation == Orientation.Vertical)
+                    {
+                        foreach (var child in VisualChildren)
+                        {
+                            double snapPoint = 0;
+
+                            switch (snapPointsAlignment)
+                            {
+                                case SnapPointsAlignment.Near:
+                                    snapPoint = child.Bounds.Top;
+                                    break;
+                                case SnapPointsAlignment.Center:
+                                    snapPoint = child.Bounds.Center.Y;
+                                    break;
+                                case SnapPointsAlignment.Far:
+                                    snapPoint = child.Bounds.Bottom;
+                                    break;
+                            }
+
+                            snapPoints.Add(snapPoint);
+                        }
+                    }
+                    break;
+            }
+
+            return snapPoints;
+        }
+
+        /// <inheritdoc/>
+        public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
+        {
+            offset = 0f;
+            var firstChild = VisualChildren.FirstOrDefault();
+
+            if(firstChild == null)
+            {
+                return 0;
+            }
+
+            double snapPoint = 0;
+
+            switch (Orientation)
+            {
+                case Orientation.Horizontal:
+                    if (!AreHorizontalSnapPointsRegular)
+                        throw new InvalidOperationException();
+
+                    snapPoint = firstChild.Bounds.Width;
+                    switch (snapPointsAlignment)
+                    {
+                        case SnapPointsAlignment.Near:
+                            offset = firstChild.Bounds.Left;
+                            break;
+                        case SnapPointsAlignment.Center:
+                            offset = firstChild.Bounds.Center.X;
+                            break;
+                        case SnapPointsAlignment.Far:
+                            offset = firstChild.Bounds.Right;
+                            break;
+                    }
+                    break;
+                case Orientation.Vertical:
+                    if (!AreVerticalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    snapPoint = firstChild.Bounds.Height;
+                    switch (snapPointsAlignment)
+                    {
+                        case SnapPointsAlignment.Near:
+                            offset = firstChild.Bounds.Top;
+                            break;
+                        case SnapPointsAlignment.Center:
+                            offset = firstChild.Bounds.Center.Y;
+                            break;
+                        case SnapPointsAlignment.Far:
+                            offset = firstChild.Bounds.Bottom;
+                            break;
+                    }
+                    break;
+            }
+
+            return snapPoint + Spacing;
+        }
     }
     }
 }
 }

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

@@ -575,12 +575,21 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         /// <param name="e">The event args.</param>
         private void HandleInput(RawInputEventArgs e)
         private void HandleInput(RawInputEventArgs e)
         {
         {
-            if (e is RawPointerEventArgs pointerArgs)
+            if (PlatformImpl != null)
             {
             {
-                pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
-            }
+                if (e is RawPointerEventArgs pointerArgs)
+                {
+                    pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
+                }
 
 
-            _inputManager?.ProcessInput(e);
+                _inputManager?.ProcessInput(e);
+            }
+            else
+            {
+                Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
+                    this,
+                    "PlatformImpl is null, couldn't handle input.");
+            }
         }
         }
 
 
         private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
         private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
@@ -600,6 +609,13 @@ namespace Avalonia.Controls
                 KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
                 KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
         }
         }
 
 
+        protected override bool BypassFlowDirectionPolicies => true;
+
+        public override void InvalidateMirrorTransform()
+        {
+            // Do nothing becuase TopLevel should't apply MirrorTransform on himself.
+        }
+
         ITextInputMethodImpl? ITextInputMethodRoot.InputMethod =>
         ITextInputMethodImpl? ITextInputMethodRoot.InputMethod =>
             (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
             (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
     }
     }

+ 231 - 1
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -3,8 +3,11 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Collections.Specialized;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Linq;
 using System.Linq;
+using System.Reflection;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
 using Avalonia.Controls.Utils;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Layout;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
@@ -14,7 +17,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// <summary>
     /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
     /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
     /// </summary>
     /// </summary>
-    public class VirtualizingStackPanel : VirtualizingPanel
+    public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
     {
     {
         /// <summary>
         /// <summary>
         /// Defines the <see cref="Orientation"/> property.
         /// Defines the <see cref="Orientation"/> property.
@@ -22,6 +25,34 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Orientation> OrientationProperty =
         public static readonly StyledProperty<Orientation> OrientationProperty =
             StackLayout.OrientationProperty.AddOwner<VirtualizingStackPanel>();
             StackLayout.OrientationProperty.AddOwner<VirtualizingStackPanel>();
 
 
+        /// <summary>
+        /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
+            AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
+
+        /// <summary>
+        /// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
+            RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
+                nameof(HorizontalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="VerticalSnapPointsChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
+            RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
+                nameof(VerticalSnapPointsChanged),
+                RoutingStrategies.Bubble);
+
         private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
         private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
             AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
             AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
 
 
@@ -62,6 +93,42 @@ namespace Avalonia.Controls
             set => SetValue(OrientationProperty, value);
             set => SetValue(OrientationProperty, value);
         }
         }
 
 
+        /// <summary>
+        /// Occurs when the measurements for horizontal snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
+        {
+            add => AddHandler(HorizontalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs when the measurements for vertical snap points change.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
+        {
+            add => AddHandler(VerticalSnapPointsChangedEvent, value);
+            remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
+        }
+
+        /// <summary>
+        /// Gets or sets whether the horizontal snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
+        /// </summary>
+        public bool AreHorizontalSnapPointsRegular
+        {
+            get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
+            set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets whether the vertical snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
+        /// </summary>
+        public bool AreVerticalSnapPointsRegular
+        {
+            get { return GetValue(AreVerticalSnapPointsRegularProperty); }
+            set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+        }
+
         protected override Size MeasureOverride(Size availableSize)
         protected override Size MeasureOverride(Size availableSize)
         {
         {
             if (!IsEffectivelyVisible)
             if (!IsEffectivelyVisible)
@@ -145,6 +212,8 @@ namespace Avalonia.Controls
             finally
             finally
             {
             {
                 _isInLayout = false;
                 _isInLayout = false;
+
+                RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
             }
             }
         }
         }
 
 
@@ -622,6 +691,167 @@ namespace Avalonia.Controls
                 Invalidate(c);
                 Invalidate(c);
         }
         }
 
 
+        /// <inheritdoc/>
+        public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
+        {
+            var snapPoints = new List<double>();
+
+            switch (orientation)
+            {
+                case Orientation.Horizontal:
+                    if (AreHorizontalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    if (Orientation == Orientation.Horizontal)
+                    {
+                        var averageElementSize = EstimateElementSizeU();
+                        double snapPoint = 0;
+                        for (var i = 0; i < Items.Count; i++)
+                        {
+                            var container = ContainerFromIndex(i);
+                            if (container != null)
+                            {
+                                switch (snapPointsAlignment)
+                                {
+                                    case SnapPointsAlignment.Near:
+                                        snapPoint = container.Bounds.Left;
+                                        break;
+                                    case SnapPointsAlignment.Center:
+                                        snapPoint = container.Bounds.Center.X;
+                                        break;
+                                    case SnapPointsAlignment.Far:
+                                        snapPoint = container.Bounds.Right;
+                                        break;
+                                }
+                            }
+                            else
+                            {
+                                if (snapPoint == 0)
+                                {
+                                    switch (snapPointsAlignment)
+                                    {
+                                        case SnapPointsAlignment.Center:
+                                            snapPoint = averageElementSize / 2;
+                                            break;
+                                        case SnapPointsAlignment.Far:
+                                            snapPoint = averageElementSize;
+                                            break;
+                                    }
+                                }
+                                else
+                                    snapPoint += averageElementSize;
+                            }
+
+                            snapPoints.Add(snapPoint);
+                        }
+                    }
+                    break;
+                case Orientation.Vertical:
+                    if (AreVerticalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    if (Orientation == Orientation.Vertical)
+                    {
+                        var averageElementSize = EstimateElementSizeU();
+                        double snapPoint = 0;
+                        for (var i = 0; i < Items.Count; i++)
+                        {
+                            var container = ContainerFromIndex(i);
+                            if (container != null)
+                            {
+                                switch (snapPointsAlignment)
+                                {
+                                    case SnapPointsAlignment.Near:
+                                        snapPoint = container.Bounds.Top;
+                                        break;
+                                    case SnapPointsAlignment.Center:
+                                        snapPoint = container.Bounds.Center.Y;
+                                        break;
+                                    case SnapPointsAlignment.Far:
+                                        snapPoint = container.Bounds.Bottom;
+                                        break;
+                                }
+                            }
+                            else
+                            {
+                                if (snapPoint == 0)
+                                {
+                                    switch (snapPointsAlignment)
+                                    {
+                                        case SnapPointsAlignment.Center:
+                                            snapPoint = averageElementSize / 2;
+                                            break;
+                                        case SnapPointsAlignment.Far:
+                                            snapPoint = averageElementSize;
+                                            break;
+                                    }
+                                }
+                                else
+                                    snapPoint += averageElementSize;
+                            }
+
+                            snapPoints.Add(snapPoint);
+                        }
+                    }
+                    break;
+            }
+
+            return snapPoints;
+        }
+
+        /// <inheritdoc/>
+        public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
+        {
+            offset = 0f;
+            var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault();
+
+            if (firstRealizedChild == null)
+            {
+                return 0;
+            }
+
+            double snapPoint = 0;
+
+            switch (Orientation)
+            {
+                case Orientation.Horizontal:
+                    if (!AreHorizontalSnapPointsRegular)
+                        throw new InvalidOperationException();
+
+                    snapPoint = firstRealizedChild.Bounds.Width;
+                    switch (snapPointsAlignment)
+                    {
+                        case SnapPointsAlignment.Near:
+                            offset = 0;
+                            break;
+                        case SnapPointsAlignment.Center:
+                            offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2;
+                            break;
+                        case SnapPointsAlignment.Far:
+                            offset = firstRealizedChild.Bounds.Width;
+                            break;
+                    }
+                    break;
+                case Orientation.Vertical:
+                    if (!AreVerticalSnapPointsRegular)
+                        throw new InvalidOperationException();
+                    snapPoint = firstRealizedChild.Bounds.Height;
+                    switch (snapPointsAlignment)
+                    {
+                        case SnapPointsAlignment.Near:
+                            offset = 0;
+                            break;
+                        case SnapPointsAlignment.Center:
+                            offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2;
+                            break;
+                        case SnapPointsAlignment.Far:
+                            offset = firstRealizedChild.Bounds.Height;
+                            break;
+                    }
+                    break;
+            }
+
+            return snapPoint;
+        }
+
         /// <summary>
         /// <summary>
         /// Stores the realized element state for a <see cref="VirtualizingStackPanel"/>.
         /// Stores the realized element state for a <see cref="VirtualizingStackPanel"/>.
         /// </summary>
         /// </summary>

+ 5 - 1
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -120,7 +120,11 @@ namespace Avalonia.Headless
             return new HeadlessGeometryStub(new Rect(glyphRun.Size));
             return new HeadlessGeometryStub(new Rect(glyphRun.Size));
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(
+            IGlyphTypeface glyphTypeface, 
+            double fontRenderingEmSize,
+            IReadOnlyList<GlyphInfo> glyphInfos, 
+            Point baselineOrigin)
         {
         {
             return new HeadlessGlyphRunStub();
             return new HeadlessGlyphRunStub();
         }
         }

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

@@ -9,6 +9,8 @@
                 CornerRadius="{TemplateBinding CornerRadius}"
                 CornerRadius="{TemplateBinding CornerRadius}"
                 Padding="{TemplateBinding Padding}">
                 Padding="{TemplateBinding Padding}">
           <ItemsPresenter Name="PART_ItemsPresenter"
           <ItemsPresenter Name="PART_ItemsPresenter"
+                          AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
+                          AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
                           ItemsPanel="{TemplateBinding ItemsPanel}"/>
                           ItemsPanel="{TemplateBinding ItemsPanel}"/>
         </Border>
         </Border>
       </ControlTemplate>
       </ControlTemplate>

+ 4 - 0
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@@ -29,11 +29,15 @@
                 BorderThickness="{TemplateBinding BorderThickness}"
                 BorderThickness="{TemplateBinding BorderThickness}"
                 CornerRadius="{TemplateBinding CornerRadius}">
                 CornerRadius="{TemplateBinding CornerRadius}">
           <ScrollViewer Name="PART_ScrollViewer"
           <ScrollViewer Name="PART_ScrollViewer"
+                        VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}"
+                        HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                         VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                         AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
                         AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
             <ItemsPresenter Name="PART_ItemsPresenter"
             <ItemsPresenter Name="PART_ItemsPresenter"
+                            AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
+                            AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
                             ItemsPanel="{TemplateBinding ItemsPanel}"
                             ItemsPanel="{TemplateBinding ItemsPanel}"
                             Margin="{TemplateBinding Padding}"/>
                             Margin="{TemplateBinding Padding}"/>
           </ScrollViewer>
           </ScrollViewer>

+ 6 - 1
src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml

@@ -34,12 +34,17 @@
                                   Content="{TemplateBinding Content}"
                                   Content="{TemplateBinding Content}"
                                   Extent="{TemplateBinding Extent, Mode=TwoWay}"
                                   Extent="{TemplateBinding Extent, Mode=TwoWay}"
                                   Padding="{TemplateBinding Padding}"
                                   Padding="{TemplateBinding Padding}"
+                                  HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
+                                  VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
+                                  HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
+                                  VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
                                   Offset="{TemplateBinding Offset, Mode=TwoWay}"
                                   Offset="{TemplateBinding Offset, Mode=TwoWay}"
                                   Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
                                   Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
                                   IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
                                   IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
             <ScrollContentPresenter.GestureRecognizers>
             <ScrollContentPresenter.GestureRecognizers>
               <ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
               <ScrollGestureRecognizer CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
-                                       CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" />
+                                       CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
+                                       IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"/>
             </ScrollContentPresenter.GestureRecognizers>
             </ScrollContentPresenter.GestureRecognizers>
           </ScrollContentPresenter>
           </ScrollContentPresenter>
           <ScrollBar Name="PART_HorizontalScrollBar"
           <ScrollBar Name="PART_HorizontalScrollBar"

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

@@ -10,6 +10,8 @@
                 BorderThickness="{TemplateBinding BorderThickness}"
                 BorderThickness="{TemplateBinding BorderThickness}"
                 CornerRadius="{TemplateBinding CornerRadius}">
                 CornerRadius="{TemplateBinding CornerRadius}">
           <ItemsPresenter Name="PART_ItemsPresenter"
           <ItemsPresenter Name="PART_ItemsPresenter"
+                          AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
+                          AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
                           ItemsPanel="{TemplateBinding ItemsPanel}" />
                           ItemsPanel="{TemplateBinding ItemsPanel}" />
         </Border>
         </Border>
       </ControlTemplate>
       </ControlTemplate>

+ 5 - 1
src/Avalonia.Themes.Simple/Controls/ListBox.xaml

@@ -20,9 +20,13 @@
                         Background="{TemplateBinding Background}"
                         Background="{TemplateBinding Background}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
-                        VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
+                        VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
+                        VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}"
+                        HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}">
             <ItemsPresenter Name="PART_ItemsPresenter"
             <ItemsPresenter Name="PART_ItemsPresenter"
                             Margin="{TemplateBinding Padding}"
                             Margin="{TemplateBinding Padding}"
+                            AreVerticalSnapPointsRegular="{TemplateBinding AreVerticalSnapPointsRegular}"
+                            AreHorizontalSnapPointsRegular="{TemplateBinding AreHorizontalSnapPointsRegular}"
                             ItemsPanel="{TemplateBinding ItemsPanel}" />
                             ItemsPanel="{TemplateBinding ItemsPanel}" />
           </ScrollViewer>
           </ScrollViewer>
         </Border>
         </Border>

+ 5 - 1
src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml

@@ -13,7 +13,11 @@
                                   Background="{TemplateBinding Background}"
                                   Background="{TemplateBinding Background}"
                                   CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
                                   CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
                                   CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
                                   CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
-                                  Content="{TemplateBinding Content}"
+                                  Content="{TemplateBinding Content}"                                  
+                                  HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
+                                  VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}"
+                                  HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
+                                  VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
                                   Extent="{TemplateBinding Extent,
                                   Extent="{TemplateBinding Extent,
                                                            Mode=TwoWay}"
                                                            Mode=TwoWay}"
                                   IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"
                                   IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"

+ 5 - 2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -201,7 +201,11 @@ namespace Avalonia.Skia
             return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
             return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(
+            IGlyphTypeface glyphTypeface,
+            double fontRenderingEmSize, 
+            IReadOnlyList<GlyphInfo> glyphInfos,
+            Point baselineOrigin)
         {
         {
             if (glyphTypeface == null)
             if (glyphTypeface == null)
             {
             {
@@ -252,7 +256,6 @@ namespace Avalonia.Skia
 
 
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
             var height = glyphTypeface.Metrics.LineSpacing * scale;
             var height = glyphTypeface.Metrics.LineSpacing * scale;
-            var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale);
 
 
             return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin);
             return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin);
         }
         }

+ 3 - 3
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -158,7 +158,8 @@ namespace Avalonia.Direct2D1
         public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
         public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
         public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
         public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
+            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
         {
         {
             var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
             var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
 
 
@@ -207,7 +208,6 @@ namespace Avalonia.Direct2D1
 
 
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
             var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
             var height = glyphTypeface.Metrics.LineSpacing * scale;
             var height = glyphTypeface.Metrics.LineSpacing * scale;
-            var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale);
 
 
             return new GlyphRunImpl(run, new Size(width, height), baselineOrigin);
             return new GlyphRunImpl(run, new Size(width, height), baselineOrigin);
         }
         }
@@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1
                 sink.Close();
                 sink.Close();
             }
             }
 
 
-            var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
+            var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
 
 
             var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
             var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
                 Direct2D1Factory,
                 Direct2D1Factory,

+ 1 - 1
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@@ -188,7 +188,7 @@ namespace Avalonia.Base.UnitTests.Media
                 glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
                 glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
             }
             }
 
 
-            return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel);
+            return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
         }
         }
     }
     }
 }
 }

+ 2 - 1
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@@ -77,7 +77,8 @@ namespace Avalonia.Base.UnitTests.VisualTree
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
+            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }

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

@@ -123,7 +123,8 @@ namespace Avalonia.Benchmarks
             return new MockStreamGeometryImpl();
             return new MockStreamGeometryImpl();
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
+            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
         {
         {
             return new MockGlyphRun(glyphInfos);
             return new MockGlyphRun(glyphInfos);
         }
         }

+ 1 - 1
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@@ -217,7 +217,7 @@ namespace Avalonia.Skia.UnitTests.Media
                 shapedBuffer.FontRenderingEmSize,
                 shapedBuffer.FontRenderingEmSize,
                 shapedBuffer.Text,
                 shapedBuffer.Text,
                 shapedBuffer.GlyphInfos,
                 shapedBuffer.GlyphInfos,
-                shapedBuffer.BidiLevel);
+                biDiLevel: shapedBuffer.BidiLevel);
 
 
             if(shapedBuffer.BidiLevel == 1)
             if(shapedBuffer.BidiLevel == 1)
             {
             {

+ 2 - 1
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -149,7 +149,8 @@ namespace Avalonia.UnitTests
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
 
 
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
+        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
+            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
         {
         {
             return new MockGlyphRun(glyphInfos);
             return new MockGlyphRun(glyphInfos);
         }
         }