diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index a7bde551e9..fdd7a3e288 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/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(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(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(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(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( + target.GetTemplateChildren().FirstOrDefault(x => x.Name == "PART_VerticalScrollBar")); + var track = Assert.IsType( + scrollbar.GetTemplateChildren().FirstOrDefault(x => x.Name == "track")); + return Assert.IsType(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; + } + } } } diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index d63327239b..eff38388e2 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/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; + } } }