123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638 |
- 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;
- namespace Avalonia.Controls.UnitTests
- {
- public class ScrollViewerTests : ScopedTestBase
- {
- private readonly MouseTestHelper _mouse = new();
- [Fact]
- public void Content_Is_Created()
- {
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = "Foo",
- };
- InitializeScrollViewer(target);
- Assert.IsType<TextBlock>(target.Presenter.Child);
- }
- [Fact]
- public void Offset_Should_Be_Coerced_To_Viewport()
- {
- var target = new ScrollViewer
- {
- Extent = new Size(20, 20),
- Viewport = new Size(10, 10),
- Offset = new Vector(12, 12)
- };
- Assert.Equal(new Vector(10, 10), target.Offset);
- }
- [Fact]
- public void Test_ScrollToHome()
- {
- var target = new ScrollViewer
- {
- Extent = new Size(50, 50),
- Viewport = new Size(10, 10),
- Offset = new Vector(25, 25)
- };
- target.ScrollToHome();
- Assert.Equal(new Vector(0, 0), target.Offset);
- }
- [Fact]
- public void Test_ScrollToEnd()
- {
- var target = new ScrollViewer
- {
- Extent = new Size(50, 50),
- Viewport = new Size(10, 10),
- Offset = new Vector(25, 25)
- };
- target.ScrollToEnd();
- Assert.Equal(new Vector(0, 40), target.Offset);
- }
- [Fact]
- public void SmallChange_Should_Be_16()
- {
- var target = new ScrollViewer();
- Assert.Equal(new Size(16, 16), target.SmallChange);
- }
- [Fact]
- public void LargeChange_Should_Be_Viewport()
- {
- var target = new ScrollViewer
- {
- Viewport = new Size(104, 143)
- };
- Assert.Equal(new Size(104, 143), target.LargeChange);
- }
- [Fact]
- public void SmallChange_Should_Come_From_ILogicalScrollable_If_Present()
- {
- var child = new Mock<Control>();
- var logicalScroll = child.As<ILogicalScrollable>();
- logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
- logicalScroll.Setup(x => x.ScrollSize).Returns(new Size(12, 43));
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = child.Object,
- };
- InitializeScrollViewer(target);
- Assert.Equal(new Size(12, 43), target.SmallChange);
- }
- [Fact]
- public void LargeChange_Should_Come_From_ILogicalScrollable_If_Present()
- {
- var child = new Mock<Control>();
- var logicalScroll = child.As<ILogicalScrollable>();
- logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
- logicalScroll.Setup(x => x.PageScrollSize).Returns(new Size(45, 67));
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = child.Object,
- };
- InitializeScrollViewer(target);
- Assert.Equal(new Size(45, 67), target.LargeChange);
- }
- [Fact]
- public void Changing_Extent_Should_Raise_ScrollChanged()
- {
- var target = new ScrollViewer();
- var root = new TestRoot(target);
- var raised = 0;
- target.Extent = new Size(100, 100);
- target.Viewport = new Size(50, 50);
- target.Offset = new Vector(10, 10);
- root.LayoutManager.ExecuteInitialLayoutPass();
- target.ScrollChanged += (s, e) =>
- {
- Assert.Equal(new Vector(11, 12), e.ExtentDelta);
- Assert.Equal(default, e.OffsetDelta);
- Assert.Equal(default, e.ViewportDelta);
- ++raised;
- };
- target.Extent = new Size(111, 112);
- Assert.Equal(0, raised);
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, raised);
- }
- [Fact]
- public void Changing_Offset_Should_Raise_ScrollChanged()
- {
- var target = new ScrollViewer();
- var root = new TestRoot(target);
- var raised = 0;
- target.Extent = new Size(100, 100);
- target.Viewport = new Size(50, 50);
- target.Offset = new Vector(10, 10);
- root.LayoutManager.ExecuteInitialLayoutPass();
- target.ScrollChanged += (s, e) =>
- {
- Assert.Equal(default, e.ExtentDelta);
- Assert.Equal(new Vector(12, 14), e.OffsetDelta);
- Assert.Equal(default, e.ViewportDelta);
- ++raised;
- };
- target.Offset = new Vector(22, 24);
- Assert.Equal(0, raised);
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, raised);
- }
- [Fact]
- public void Changing_Viewport_Should_Raise_ScrollChanged()
- {
- var target = new ScrollViewer();
- var root = new TestRoot(target);
- var raised = 0;
- target.Extent = new Size(100, 100);
- target.Viewport = new Size(50, 50);
- target.Offset = new Vector(10, 10);
- root.LayoutManager.ExecuteInitialLayoutPass();
- target.ScrollChanged += (s, e) =>
- {
- Assert.Equal(default, e.ExtentDelta);
- Assert.Equal(default, e.OffsetDelta);
- Assert.Equal(new Vector(6, 8), e.ViewportDelta);
- ++raised;
- };
- target.Viewport = new Size(56, 58);
- Assert.Equal(0, raised);
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, raised);
- }
- [Fact]
- public void Reducing_Extent_Should_Constrain_Offset()
- {
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- };
- var root = new TestRoot(target);
- var raised = 0;
- target.Extent = new (100, 100);
- target.Viewport = new(50, 50);
- target.Offset = new Vector(50, 50);
- root.LayoutManager.ExecuteInitialLayoutPass();
- target.ScrollChanged += (s, e) =>
- {
- Assert.Equal(new Vector(-30, -30), e.ExtentDelta);
- Assert.Equal(new Vector(-30, -30), e.OffsetDelta);
- Assert.Equal(default, e.ViewportDelta);
- ++raised;
- };
- target.Extent = new(70, 70);
- Assert.Equal(0, raised);
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, raised);
- 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);
- }
- [Fact]
- public void Thumb_Does_Not_Become_Detached_From_Mouse_Position_When_Scrolling_Past_The_Start()
- {
- var content = new TestContent();
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = content,
- };
- var root = new TestRoot(target);
- root.LayoutManager.ExecuteInitialLayoutPass();
- Assert.Equal(new Size(1000, 2000), 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 100 pixels.
- _mouse.Move(thumb, p += new Vector(0, 100));
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(new Vector(0, 200), target.Offset);
- Assert.Equal(100, thumb.Bounds.Top);
- // Drag the thumb up 200 pixels - 100 pixels past the top of the scrollbar.
- _mouse.Move(thumb, p -= new Vector(0, 200));
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(new Vector(0, 0), target.Offset);
- Assert.Equal(0, thumb.Bounds.Top);
- // Drag the thumb back down 200 pixels.
- _mouse.Move(thumb, p += new Vector(0, 200));
- root.LayoutManager.ExecuteLayoutPass();
- // We should now be back in the state after we first scrolled down 100 pixels.
- Assert.Equal(new Vector(0, 200), target.Offset);
- Assert.Equal(100, thumb.Bounds.Top);
- }
- [Fact]
- public void Deferred_Scrolling_Defers_Scrolling_Until_Pointer_Up()
- {
- var content = new TestContent();
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- IsDeferredScrollingEnabled = true,
- Content = content,
- };
- var root = new TestRoot(target);
- root.LayoutManager.ExecuteInitialLayoutPass();
- // 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);
- Assert.Equal(Vector.Zero, target.Offset);
- Assert.Equal(0, thumb.Bounds.Top);
- // Press the mouse button in the center of the thumb.
- _mouse.Down(thumb, position: p);
- root.LayoutManager.ExecuteLayoutPass();
- // Drag the thumb down 100 pixels.
- _mouse.Move(thumb, p += new Vector(0, 100));
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(Vector.Zero, target.Offset); // no change to scroll...
- Assert.Equal(100, thumb.Bounds.Top); // ...but the Thumb has moved
- // Release the mouse
- _mouse.Up(thumb, position: p);
- Assert.Equal(new Vector(0, 200), target.Offset);
- Assert.Equal(100, thumb.Bounds.Top);
- }
- [Fact]
- public void BringIntoViewOnFocusChange_Scrolls_Child_Control_Into_View_When_Focused()
- {
- using var app = UnitTestApplication.Start(TestServices.RealFocus);
- var content = new StackPanel
- {
- Children =
- {
- new Button
- {
- Width = 100,
- Height = 900,
- },
- new Button
- {
- Width = 100,
- Height = 900,
- },
- }
- };
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = content,
- };
- var root = new TestRoot(target);
- root.LayoutManager.ExecuteInitialLayoutPass();
- var button = (Button)content.Children[1];
- button.Focus();
- Assert.Equal(new Vector(0, 800), target.Offset);
- }
- [Fact]
- public void BringIntoViewOnFocusChange_False_Does_Not_Scroll_Child_Control_Into_View_When_Focused()
- {
- var content = new StackPanel
- {
- Children =
- {
- new Button
- {
- Width = 100,
- Height = 900,
- },
- new Button
- {
- Width = 100,
- Height = 900,
- },
- }
- };
- var target = new ScrollViewer
- {
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- Content = content,
- };
- var root = new TestRoot(target);
- root.LayoutManager.ExecuteInitialLayoutPass();
- var button = (Button)content.Children[1];
- button.Focus();
- Assert.Equal(new Vector(0, 0), target.Offset);
- }
- [Fact]
- public void MenuScrollBar_Should_Be_Visible_When_Specified_Visible()
- {
- Converters.MenuScrollingVisibilityConverter converter = Converters.MenuScrollingVisibilityConverter.Instance;
- IList<object> args = new List<object> {ScrollBarVisibility.Visible,400d,1800d,500d};
- var result = converter.Convert(args, typeof(ScrollBarVisibility), "0", System.Globalization.CultureInfo.CurrentCulture);
- Assert.Equal(true, result);
- }
- [Fact]
- public void ScrollBar_Visibility_Should_Invalidate_Measure_And_Arrange()
- {
- var panel = new TestPanel()
- {
- DesiredWidth = 100_000
- };
- var target = new ScrollViewer
- {
- Content = panel,
- Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
- HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
- };
- var root = new TestRoot(target);
- root.LayoutManager.ExecuteInitialLayoutPass();
- panel.Reset();
- target.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, panel.MeasureOverrideCalls);
- Assert.Equal(1, panel.ArrangeOverrideCalls);
- }
- public class TestPanel : Panel
- {
- public int DesiredWidth { get; set; }
- public int MeasureOverrideCalls { get; private set; }
- public int ArrangeOverrideCalls { get; private set; }
- protected override Size MeasureOverride(Size availableSize)
- {
- MeasureOverrideCalls++;
- return new Size(DesiredWidth, 1);
- }
- protected override Size ArrangeOverride(Size finalSize)
- {
- ArrangeOverrideCalls++;
- return base.ArrangeOverride(finalSize);
- }
- public void Reset()
- {
- MeasureOverrideCalls = 0;
- ArrangeOverrideCalls = 0;
- }
- }
- 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.");
- }
- internal static Control CreateTemplate(ScrollViewer control, INameScope scope)
- {
- return new Grid
- {
- ColumnDefinitions = new ColumnDefinitions
- {
- new ColumnDefinition(1, GridUnitType.Star),
- new ColumnDefinition(GridLength.Auto),
- },
- RowDefinitions = new RowDefinitions
- {
- new RowDefinition(1, GridUnitType.Star),
- new RowDefinition(GridLength.Auto),
- },
- Children =
- {
- new ScrollContentPresenter
- {
- Name = "PART_ContentPresenter",
- }.RegisterInNameScope(scope),
- new ScrollBar
- {
- Name = "PART_HorizontalScrollBar",
- Orientation = Orientation.Horizontal,
- Template = new FuncControlTemplate<ScrollBar>(CreateScrollBarTemplate),
- [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
- [Grid.RowProperty] = 1,
- }.RegisterInNameScope(scope),
- new ScrollBar
- {
- Name = "PART_VerticalScrollBar",
- Orientation = Orientation.Vertical,
- Template = new FuncControlTemplate<ScrollBar>(CreateScrollBarTemplate),
- [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
- [Grid.ColumnProperty] = 1,
- }.RegisterInNameScope(scope),
- },
- };
- }
- private static 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],
- [!Track.DeferThumbDragProperty] = scrollBar.TemplatedParent[!ScrollViewer.IsDeferredScrollingEnabledProperty],
- 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();
- var presenter = (ScrollContentPresenter)target.Presenter;
- 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;
- }
- }
- }
- }
|