diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
index 0b21a1f8b9..5ba6b6b9a3 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
@@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
+using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
{
@@ -99,9 +100,14 @@ namespace Avalonia.Controls.Presenters
{
get
{
- return Vertical ?
- new Size(Owner.Panel.DesiredSize.Width, ExtentValue) :
- new Size(ExtentValue, Owner.Panel.DesiredSize.Height);
+ if (IsLogicalScrollEnabled)
+ {
+ return Vertical ?
+ new Size(Owner.Panel.DesiredSize.Width, ExtentValue) :
+ new Size(ExtentValue, Owner.Panel.DesiredSize.Height);
+ }
+
+ return default;
}
}
@@ -112,9 +118,14 @@ namespace Avalonia.Controls.Presenters
{
get
{
- return Vertical ?
- new Size(Owner.Panel.Bounds.Width, ViewportValue) :
- new Size(ViewportValue, Owner.Panel.Bounds.Height);
+ if (IsLogicalScrollEnabled)
+ {
+ return Vertical ?
+ new Size(Owner.Panel.Bounds.Width, ViewportValue) :
+ new Size(ViewportValue, Owner.Panel.Bounds.Height);
+ }
+
+ return default;
}
}
@@ -125,11 +136,21 @@ namespace Avalonia.Controls.Presenters
{
get
{
- return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset);
+ if (IsLogicalScrollEnabled)
+ {
+ return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset);
+ }
+
+ return default;
}
set
{
+ if (!IsLogicalScrollEnabled)
+ {
+ throw new NotSupportedException("Logical scrolling disabled.");
+ }
+
var oldCrossAxisOffset = _crossAxisOffset;
if (Vertical)
@@ -164,10 +185,10 @@ namespace Avalonia.Controls.Presenters
}
var virtualizingPanel = owner.Panel as IVirtualizingPanel;
- var scrollable = (ILogicalScrollable)owner;
+ var scrollContentPresenter = owner.Parent as IScrollable;
ItemVirtualizer result = null;
- if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
+ if (virtualizingPanel != null && scrollContentPresenter is object)
{
switch (owner.VirtualizationMode)
{
@@ -277,6 +298,6 @@ namespace Avalonia.Controls.Presenters
///
/// Invalidates the current scroll.
///
- protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll?.Invoke();
+ protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty);
}
}
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
index 5943309cbb..02f2e7c9d9 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
@@ -21,6 +21,7 @@ namespace Avalonia.Controls.Presenters
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
+ EventHandler _scrollInvalidated;
///
/// Initializes static members of the class.
@@ -95,13 +96,17 @@ namespace Avalonia.Controls.Presenters
Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
///
- Action ILogicalScrollable.InvalidateScroll { get; set; }
+ event EventHandler ILogicalScrollable.ScrollInvalidated
+ {
+ add => _scrollInvalidated += value;
+ remove => _scrollInvalidated -= value;
+ }
///
- Size ILogicalScrollable.ScrollSize => new Size(1, 1);
+ Size ILogicalScrollable.ScrollSize => new Size(16, 1);
///
- Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
+ Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16);
internal ItemVirtualizer Virtualizer { get; private set; }
@@ -117,6 +122,12 @@ namespace Avalonia.Controls.Presenters
return Virtualizer?.GetControlInDirection(direction, from);
}
+ ///
+ void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e)
+ {
+ _scrollInvalidated?.Invoke(this, e);
+ }
+
public override void ScrollIntoView(object item)
{
Virtualizer?.ScrollIntoView(item);
@@ -138,7 +149,7 @@ namespace Avalonia.Controls.Presenters
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
- ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
+ _scrollInvalidated?.Invoke(this, EventArgs.Empty);
KeyboardNavigation.SetTabNavigation(
(InputElement)Panel,
@@ -162,7 +173,7 @@ namespace Avalonia.Controls.Presenters
{
Virtualizer?.Dispose();
Virtualizer = ItemVirtualizer.Create(this);
- ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
+ _scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
}
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index c0391c89ca..98a5b10023 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
+using System.Runtime.InteropServices.ComTypes;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
@@ -349,7 +351,7 @@ namespace Avalonia.Controls.Presenters
if (scrollable != null)
{
- scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
+ scrollable.ScrollInvalidated += ScrollInvalidated;
if (scrollable.IsLogicalScrollEnabled)
{
@@ -360,12 +362,17 @@ namespace Avalonia.Controls.Presenters
.Subscribe(x => scrollable.CanVerticallyScroll = x),
this.GetObservable(OffsetProperty)
.Skip(1).Subscribe(x => scrollable.Offset = x),
- Disposable.Create(() => scrollable.InvalidateScroll = null));
+ Disposable.Create(() => scrollable.ScrollInvalidated -= ScrollInvalidated));
UpdateFromScrollable(scrollable);
}
}
}
+ private void ScrollInvalidated(object sender, EventArgs e)
+ {
+ UpdateFromScrollable((ILogicalScrollable)sender);
+ }
+
private void UpdateFromScrollable(ILogicalScrollable scrollable)
{
var logicalScroll = _logicalScrollSubscription != null;
diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
index 6e8d097dd1..5c29945735 100644
--- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
+++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
@@ -31,22 +31,6 @@ namespace Avalonia.Controls.Primitives
///
bool IsLogicalScrollEnabled { get; }
- ///
- /// Gets or sets the scroll invalidation method.
- ///
- ///
- ///
- /// This method notifies the attached of a change in
- /// the , or
- /// properties.
- ///
- ///
- /// This property is set by the parent when the
- /// is placed inside it.
- ///
- ///
- Action InvalidateScroll { get; set; }
-
///
/// Gets the size to scroll by, in logical units.
///
@@ -57,6 +41,15 @@ namespace Avalonia.Controls.Primitives
///
Size PageScrollSize { get; }
+ ///
+ /// Raised when the scroll is invalidated.
+ ///
+ ///
+ /// This event notifies an attached of a change in
+ /// one of the scroll properties.
+ ///
+ event EventHandler ScrollInvalidated;
+
///
/// Attempts to bring a portion of the target visual into view by scrolling the content.
///
@@ -72,5 +65,11 @@ namespace Avalonia.Controls.Primitives
/// The control from which movement begins.
/// The control.
IControl GetControlInDirection(NavigationDirection direction, IControl from);
+
+ ///
+ /// Raises the event.
+ ///
+ /// The event args.
+ void RaiseScrollInvalidated(EventArgs e);
}
}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index b1f340bd65..f5881a8efe 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Controls
///
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
+ private static readonly Size s_defaultSmallChange = new Size(16, 16);
+
///
/// Defines the property.
///
@@ -59,6 +61,22 @@ namespace Avalonia.Controls
o => o.Viewport,
(o, v) => o.Viewport = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty LargeChangeProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(LargeChange),
+ o => o.LargeChange);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty SmallChangeProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SmallChange),
+ o => o.SmallChange);
+
///
/// Defines the HorizontalScrollBarMaximum property.
///
@@ -149,9 +167,13 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
+ private IDisposable _childSubscription;
+ private ILogicalScrollable _logicalScrollable;
private Size _extent;
private Vector _offset;
private Size _viewport;
+ private Size _largeChange;
+ private Size _smallChange = s_defaultSmallChange;
///
/// Initializes static members of the class.
@@ -228,6 +250,16 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Gets the large (page) change value for the scroll viewer.
+ ///
+ public Size LargeChange => _largeChange;
+
+ ///
+ /// Gets the small (line) change value for the scroll viewer.
+ ///
+ public Size SmallChange => _smallChange;
+
///
/// Gets or sets the horizontal scrollbar visibility.
///
@@ -246,22 +278,6 @@ namespace Avalonia.Controls
set { SetValue(VerticalScrollBarVisibilityProperty, value); }
}
- ///
- /// Scrolls to the top-left corner of the content.
- ///
- public void ScrollToHome()
- {
- Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity);
- }
-
- ///
- /// Scrolls to the bottom-left corner of the content.
- ///
- public void ScrollToEnd()
- {
- Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity);
- }
-
///
/// Gets a value indicating whether the viewer can scroll horizontally.
///
@@ -347,6 +363,22 @@ namespace Avalonia.Controls
///
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
+ ///
+ /// Scrolls to the top-left corner of the content.
+ ///
+ public void ScrollToHome()
+ {
+ Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity);
+ }
+
+ ///
+ /// Scrolls to the bottom-left corner of the content.
+ ///
+ public void ScrollToEnd()
+ {
+ Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity);
+ }
+
///
/// Gets the value of the HorizontalScrollBarVisibility attached property.
///
@@ -397,6 +429,22 @@ namespace Avalonia.Controls
// TODO: Implement
}
+ protected override bool RegisterContentPresenter(IContentPresenter presenter)
+ {
+ _childSubscription?.Dispose();
+ _childSubscription = null;
+
+ if (base.RegisterContentPresenter(presenter))
+ {
+ _childSubscription = Presenter?
+ .GetObservable(ContentPresenter.ChildProperty)
+ .Subscribe(ChildChanged);
+ return true;
+ }
+
+ return false;
+ }
+
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);
@@ -431,6 +479,28 @@ namespace Avalonia.Controls
}
}
+ private void ChildChanged(IControl child)
+ {
+ if (_logicalScrollable is object)
+ {
+ _logicalScrollable.ScrollInvalidated -= LogicalScrollInvalidated;
+ _logicalScrollable = null;
+ }
+
+ if (child is ILogicalScrollable logical)
+ {
+ _logicalScrollable = logical;
+ logical.ScrollInvalidated += LogicalScrollInvalidated;
+ }
+
+ CalculatedPropertiesChanged();
+ }
+
+ private void LogicalScrollInvalidated(object sender, EventArgs e)
+ {
+ CalculatedPropertiesChanged();
+ }
+
private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
{
var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue);
@@ -465,6 +535,17 @@ namespace Avalonia.Controls
RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum);
RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue);
RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize);
+
+ if (_logicalScrollable?.IsLogicalScrollEnabled == true)
+ {
+ SetAndRaise(SmallChangeProperty, ref _smallChange, _logicalScrollable.ScrollSize);
+ SetAndRaise(LargeChangeProperty, ref _largeChange, _logicalScrollable.PageScrollSize);
+ }
+ else
+ {
+ SetAndRaise(SmallChangeProperty, ref _smallChange, s_defaultSmallChange);
+ SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
+ }
}
protected override void OnKeyDown(KeyEventArgs e)
diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml
index 38f4eef964..1d893133e1 100644
--- a/src/Avalonia.Themes.Default/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml
@@ -22,6 +22,8 @@
_scrollInvalidated != null;
+
+ public event EventHandler ScrollInvalidated
+ {
+ add => _scrollInvalidated += value;
+ remove => _scrollInvalidated -= value;
+ }
public Size Extent
{
@@ -331,7 +339,7 @@ namespace Avalonia.Controls.UnitTests
set
{
_extent = value;
- InvalidateScroll?.Invoke();
+ _scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
@@ -341,7 +349,7 @@ namespace Avalonia.Controls.UnitTests
set
{
_offset = value;
- InvalidateScroll?.Invoke();
+ _scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
@@ -351,7 +359,7 @@ namespace Avalonia.Controls.UnitTests
set
{
_viewport = value;
- InvalidateScroll?.Invoke();
+ _scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
@@ -376,6 +384,11 @@ namespace Avalonia.Controls.UnitTests
throw new NotImplementedException();
}
+ public void RaiseScrollInvalidated(EventArgs e)
+ {
+ _scrollInvalidated?.Invoke(this, e);
+ }
+
protected override Size MeasureOverride(Size availableSize)
{
AvailableSize = availableSize;
@@ -388,4 +401,4 @@ namespace Avalonia.Controls.UnitTests
}
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index f58a4cbffe..5375a244c9 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -4,6 +4,8 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
+using Avalonia.LogicalTree;
+using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -86,6 +88,65 @@ namespace Avalonia.Controls.UnitTests
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();
+
+ target.SetValue(ScrollViewer.ViewportProperty, 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();
+ var logicalScroll = child.As();
+
+ logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
+ logicalScroll.Setup(x => x.ScrollSize).Returns(new Size(12, 43));
+
+ var target = new ScrollViewer
+ {
+ Template = new FuncControlTemplate(CreateTemplate),
+ Content = child.Object,
+ };
+
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ Assert.Equal(new Size(12, 43), target.SmallChange);
+ }
+
+ [Fact]
+ public void LargeChange_Should_Come_From_ILogicalScrollable_If_Present()
+ {
+ var child = new Mock();
+ var logicalScroll = child.As();
+
+ logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
+ logicalScroll.Setup(x => x.PageScrollSize).Returns(new Size(45, 67));
+
+ var target = new ScrollViewer
+ {
+ Template = new FuncControlTemplate(CreateTemplate),
+ Content = child.Object,
+ };
+
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ Assert.Equal(new Size(45, 67), target.LargeChange);
+ }
+
private Control CreateTemplate(ScrollViewer control, INameScope scope)
{
return new Grid