浏览代码

Added failing test for ScrollViewer jumpiness.

Steven Kirk 2 年之前
父节点
当前提交
42bd366bc1
共有 2 个文件被更改,包括 129 次插入3 次删除
  1. 122 0
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  2. 7 3
      tests/Avalonia.UnitTests/MouseTestHelper.cs

+ 122 - 0
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -1,10 +1,13 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Layout;
+using Avalonia.Media;
 using Avalonia.UnitTests;
+using Avalonia.VisualTree;
 using Moq;
 using Xunit;
 
@@ -12,6 +15,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class ScrollViewerTests
     {
+        private readonly MouseTestHelper _mouse = new();
+
         [Fact]
         public void Content_Is_Created()
         {
@@ -249,6 +254,73 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Vector(20, 20), target.Offset); 
         }
 
+        [Fact]
+        public void Scroll_Does_Not_Jump_When_Viewport_Becomes_Smaller_While_Dragging_ScrollBar_Thumb()
+        {
+            var content = new TestContent
+            {
+                MeasureSize = new Size(1000, 10000),
+            };
+
+            var target = new ScrollViewer
+            {
+                Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
+                Content = content,
+            };
+            var root = new TestRoot(target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            Assert.Equal(new Size(1000, 10000), target.Extent);
+            Assert.Equal(new Size(1000, 1000), target.Viewport);
+
+            // We're working in absolute coordinates (i.e. relative to the root) and clicking on
+            // the center of the vertical thumb.
+            var thumb = GetVerticalThumb(target);
+            var p = GetRootPoint(thumb, thumb.Bounds.Center);
+
+            // Press the mouse button in the center of the thumb.
+            _mouse.Down(thumb, position: p);
+            root.LayoutManager.ExecuteLayoutPass();
+
+            // Drag the thumb down 300 pixels.
+            _mouse.Move(thumb, p += new Vector(0, 300));
+            root.LayoutManager.ExecuteLayoutPass();
+
+            Assert.Equal(new Vector(0, 3000), target.Offset);
+            Assert.Equal(300, thumb.Bounds.Top);
+
+            // Now the extent changes from 10,000 to 5000.
+            content.MeasureSize /= 2;
+            content.InvalidateMeasure();
+            root.LayoutManager.ExecuteLayoutPass();
+
+            // Due to the extent change, the thumb moves down but the value remains the same.
+            Assert.Equal(600, thumb.Bounds.Top);
+            Assert.Equal(new Vector(0, 3000), target.Offset);
+
+            // Drag the thumb down another 100 pixels.
+            _mouse.Move(thumb, p += new Vector(0, 100));
+            root.LayoutManager.ExecuteLayoutPass();
+
+            // The drag should not cause the offset/thumb to jump *up* to the current absolute
+            // mouse position, i.e. it should move down in the direction of the drag even if the
+            // absolute mouse position is now above the thumb.
+            Assert.Equal(700, thumb.Bounds.Top);
+            Assert.Equal(new Vector(0, 3500), target.Offset);
+        }
+
+        private Point GetRootPoint(Visual control, Point p)
+        {
+            if (control.GetVisualRoot() is Visual root &&
+                control.TransformToVisual(root) is Matrix m)
+            {
+                return p.Transform(m);
+            }
+
+            throw new InvalidOperationException("Could not get the point in root coordinates.");
+        }
+
         private Control CreateTemplate(ScrollViewer control, INameScope scope)
         {
             return new Grid
@@ -273,6 +345,7 @@ namespace Avalonia.Controls.UnitTests
                     {
                         Name = "PART_HorizontalScrollBar",
                         Orientation = Orientation.Horizontal,
+                        Template = new FuncControlTemplate<ScrollBar>(CreateScrollBarTemplate),
                         [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
                         [Grid.RowProperty] = 1,
                     }.RegisterInNameScope(scope),
@@ -280,6 +353,7 @@ namespace Avalonia.Controls.UnitTests
                     {
                         Name = "PART_VerticalScrollBar",
                         Orientation = Orientation.Vertical,
+                        Template = new FuncControlTemplate<ScrollBar>(CreateScrollBarTemplate),
                         [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
                         [Grid.ColumnProperty] = 1,
                     }.RegisterInNameScope(scope),
@@ -287,6 +361,44 @@ namespace Avalonia.Controls.UnitTests
             };
         }
 
+        private Control CreateScrollBarTemplate(ScrollBar scrollBar, INameScope scope)
+        {
+            return new Border
+            {
+                Child = new Track
+                {
+                    Name = "track",
+                    IsDirectionReversed = true,
+                    [!Track.MinimumProperty] = scrollBar[!RangeBase.MinimumProperty],
+                    [!Track.MaximumProperty] = scrollBar[!RangeBase.MaximumProperty],
+                    [!!Track.ValueProperty] = scrollBar[!!RangeBase.ValueProperty],
+                    [!Track.ViewportSizeProperty] = scrollBar[!ScrollBar.ViewportSizeProperty],
+                    [!Track.OrientationProperty] = scrollBar[!ScrollBar.OrientationProperty],
+                    Thumb = new Thumb
+                    {
+                        Template = new FuncControlTemplate<Thumb>(CreateThumbTemplate),
+                    },
+                }.RegisterInNameScope(scope),
+            };
+        }
+
+        private static Control CreateThumbTemplate(Thumb control, INameScope scope)
+        {
+            return new Border
+            {
+                Background = Brushes.Gray,
+            };
+        }
+
+        private Thumb GetVerticalThumb(ScrollViewer target)
+        {
+            var scrollbar = Assert.IsType<ScrollBar>(
+                target.GetTemplateChildren().FirstOrDefault(x => x.Name == "PART_VerticalScrollBar"));
+            var track = Assert.IsType<Track>(
+                scrollbar.GetTemplateChildren().FirstOrDefault(x => x.Name == "track"));
+            return Assert.IsType<Thumb>(track.Thumb);
+        }
+
         private static void InitializeScrollViewer(ScrollViewer target)
         {
             target.ApplyTemplate();
@@ -295,5 +407,15 @@ namespace Avalonia.Controls.UnitTests
             presenter.AttachToScrollViewer();
             presenter.UpdateChild();
         }
+
+        private class TestContent : Control
+        {
+            public Size MeasureSize { get; set; } = new Size(1000, 2000);
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                return MeasureSize;
+            }
+        }
     }
 }

+ 7 - 3
tests/Avalonia.UnitTests/MouseTestHelper.cs

@@ -59,7 +59,7 @@ namespace Avalonia.UnitTests
             {
                 _pressedButton = mouseButton;
                 _pointer.Capture((IInputElement)target);
-                source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (Visual)source, position, Timestamp(), props,
+                source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, GetRoot(target), position, Timestamp(), props,
                     modifiers, clickCount));
             }
         }
@@ -68,7 +68,7 @@ namespace Avalonia.UnitTests
 
         public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default)
         {
-            target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position,
+            target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, GetRoot(target), position,
                 Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), modifiers));
         }
 
@@ -88,7 +88,7 @@ namespace Avalonia.UnitTests
             );
             if (ButtonCount(props) == 0)
             {
-                target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (Visual)target, position,
+                target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, GetRoot(target), position,
                     Timestamp(), props, modifiers, _pressedButton));
                 _pointer.Capture(null);
             }
@@ -131,5 +131,9 @@ namespace Avalonia.UnitTests
                 Timestamp(), new PointerPointProperties((RawInputModifiers)_pressedButtons, PointerUpdateKind.Other), KeyModifiers.None));
         }
 
+        private Visual GetRoot(Interactive source)
+        {
+            return ((source as Visual)?.GetVisualRoot() as Visual) ?? (Visual)source;
+        }
     }
 }