diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 9ec70a4eb1..76f4cdd106 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -14,6 +14,7 @@ namespace XamlTestApplication private Size _viewport; private Size _lineSize; + public bool IsLogicalScrollEnabled => true; public Action InvalidateScroll { get; set; } Size IScrollable.Extent diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0077497b10..abb606435d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls.Presenters { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); - this.GetObservable(ChildProperty).Subscribe(ChildChanged); + this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } /// @@ -194,22 +194,25 @@ namespace Avalonia.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; - var offset = default(Vector); + var logicalScroll = _scrollableSubscription != null; - if (_scrollableSubscription == null) + if (!logicalScroll) { Viewport = finalSize; Extent = _measuredExtent; - offset = Offset; - } - if (child != null) - { - var size = new Size( + if (child != null) + { + var size = new Size( Math.Max(finalSize.Width, child.DesiredSize.Width), Math.Max(finalSize.Height, child.DesiredSize.Height)); - child.Arrange(new Rect((Point)(-offset), size)); - return finalSize; + child.Arrange(new Rect((Point)(-Offset), size)); + return finalSize; + } + } + else if (child != null) + { + child.Arrange(new Rect(finalSize)); } return new Size(); @@ -222,7 +225,7 @@ namespace Avalonia.Controls.Presenters { var scrollable = Child as IScrollable; - if (scrollable != null) + if (scrollable?.IsLogicalScrollEnabled == true) { var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height); y = Math.Max(y, 0); @@ -246,7 +249,7 @@ namespace Avalonia.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } - private void ChildChanged(IControl child) + private void UpdateScrollableSubscription(IControl child) { var scrollable = child as IScrollable; @@ -256,18 +259,38 @@ namespace Avalonia.Controls.Presenters if (scrollable != null) { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - _scrollableSubscription = new CompositeDisposable( - this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), - Disposable.Create(() => scrollable.InvalidateScroll = null)); - UpdateFromScrollable(scrollable); + + if (scrollable?.IsLogicalScrollEnabled == true) + { + _scrollableSubscription = new CompositeDisposable( + this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + Disposable.Create(() => scrollable.InvalidateScroll = null)); + UpdateFromScrollable(scrollable); + } } } private void UpdateFromScrollable(IScrollable scrollable) { - Viewport = scrollable.Viewport; - Extent = scrollable.Extent; - Offset = scrollable.Offset; + var logicalScroll = _scrollableSubscription != null; + + if (logicalScroll != scrollable.IsLogicalScrollEnabled) + { + UpdateScrollableSubscription(Child); + + if (!scrollable.IsLogicalScrollEnabled) + { + Offset = default(Vector); + InvalidateMeasure(); + } + } + + if (scrollable.IsLogicalScrollEnabled) + { + Viewport = scrollable.Viewport; + Extent = scrollable.Extent; + Offset = scrollable.Offset; + } } } } diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 00f0c92535..2171e87c51 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -23,6 +23,7 @@ namespace Avalonia.Controls.Presenters public IPanel Panel => _panel; + bool IScrollable.IsLogicalScrollEnabled => true; Action IScrollable.InvalidateScroll { get; set; } Size IScrollable.Extent => new Size(1, 100 * AverageItemSize ); diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs index a5165fcfb4..d37d1fdcca 100644 --- a/src/Avalonia.Controls/Primitives/IScrollable.cs +++ b/src/Avalonia.Controls/Primitives/IScrollable.cs @@ -9,8 +9,20 @@ namespace Avalonia.Controls.Primitives /// Interface implemented by controls that handle their own scrolling when placed inside a /// . /// + /// + /// Controls that implement this interface, when placed inside a + /// can override the physical scrolling behavior of the scroll viewer with logical scrolling. + /// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas + /// whereas logical scrolling means that the scrolling is handled by the child control itself + /// and it can choose to do handle the scroll information as it sees fit. + /// public interface IScrollable { + /// + /// Gets a value indicating whether logical scrolling is enabled on the control. + /// + bool IsLogicalScrollEnabled { get; } + /// /// Gets or sets the scroll invalidation method. /// diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index db3b2a4a19..1aea155eaa 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Layout; using Xunit; namespace Avalonia.Controls.UnitTests @@ -48,6 +49,27 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } + [Fact] + public void Arrange_Should_Offset_IScrollable_Bounds_When_Logical_Scroll_Disabled() + { + var scrollable = new TestScrollable + { + IsLogicalScrollEnabled = false, + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + Offset = new Vector(25, 25), + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds); + } + [Fact] public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable() { @@ -169,12 +191,59 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(50, 50), scrollable.Offset); } + [Fact] + public void Toggling_IsLogicalScrollEnabled_Should_Update_State() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + + scrollable.IsLogicalScrollEnabled = false; + scrollable.InvalidateScroll(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(150, 150), target.Extent); + Assert.Equal(new Vector(0, 0), target.Offset); + Assert.Equal(new Size(100, 100), target.Viewport); + Assert.Equal(new Rect(0, 0, 150, 150), scrollable.Bounds); + + scrollable.IsLogicalScrollEnabled = true; + scrollable.InvalidateScroll(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + } + + private class TestScrollable : Control, IScrollable { private Size _extent; private Vector _offset; private Size _viewport; + public bool IsLogicalScrollEnabled { get; set; } = true; public Size AvailableSize { get; private set; } public Action InvalidateScroll { get; set; }