From bfc010f757afe1f404b4d297c3cbce03215509bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Dec 2015 20:18:28 +0000 Subject: [PATCH] Initial implementation of logical scrolling. aka IScrollInfo in WPF. --- .../XamlTestApplicationPcl/TestScrollable.cs | 92 ++++++++ .../Views/MainWindow.paml | 6 + .../XamlTestApplicationPcl.csproj | 1 + src/Perspex.Controls/ContentControl.cs | 2 - src/Perspex.Controls/Perspex.Controls.csproj | 3 +- .../Presenters/ScrollContentPresenter.cs | 60 ++++- .../Primitives/IScrollInfo.cs | 97 -------- .../Primitives/IScrollable.cs | 39 ++++ .../Primitives/ScrollInfoAdapter.cs | 126 ---------- src/Perspex.Controls/ScrollViewer.cs | 5 + .../Perspex.Controls.UnitTests.csproj | 1 + .../Presenters/ScrollContentPresenterTests.cs | 32 +++ ...ScrollContentPresenterTests_IScrollable.cs | 216 ++++++++++++++++++ 13 files changed, 443 insertions(+), 237 deletions(-) create mode 100644 samples/XamlTestApplicationPcl/TestScrollable.cs delete mode 100644 src/Perspex.Controls/Primitives/IScrollInfo.cs create mode 100644 src/Perspex.Controls/Primitives/IScrollable.cs delete mode 100644 src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs create mode 100644 tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs new file mode 100644 index 0000000000..9a0bc8de73 --- /dev/null +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -0,0 +1,92 @@ +using System; +using Perspex; +using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Media; + +namespace XamlTestApplication +{ + public class TestScrollable : Control, IScrollable + { + private int itemCount = 100; + private Action _invalidateScroll; + private Size _extent; + private Vector _offset; + private Size _viewport; + private Size _lineSize; + + public Action InvalidateScroll + { + get { return _invalidateScroll; } + set { _invalidateScroll = value; } + } + + Size IScrollable.Extent + { + get { return _extent; } + } + + Vector IScrollable.Offset + { + get + { + return _offset; + } + + set + { + _offset = value; + InvalidateVisual(); + } + } + + Size IScrollable.Viewport + { + get { return _viewport; } + } + + protected override Size MeasureOverride(Size availableSize) + { + using (var line = new FormattedText( + "Item 100", + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + line.Constraint = availableSize; + _lineSize = line.Measure(); + return new Size(_lineSize.Width, _lineSize.Height * itemCount); + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + _viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height); + _extent = new Size(_lineSize.Width, itemCount); + InvalidateScroll(); + return finalSize; + } + + public override void Render(DrawingContext context) + { + var y = 0.0; + + for (var i = (int)_offset.Y; i < itemCount; ++i) + { + using (var line = new FormattedText( + "Item " + (i + 1), + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); + y += _lineSize.Height; + } + } + } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index c19220deb7..12440b4106 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -1,6 +1,7 @@  @@ -272,6 +273,11 @@ + + + + + \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj index 46358e5836..a32ae5eec6 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj +++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index 2899b58bcd..e8b52a2948 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -36,8 +36,6 @@ namespace Perspex.Controls public static readonly PerspexProperty VerticalContentAlignmentProperty = PerspexProperty.Register(nameof(VerticalContentAlignment)); - private IDisposable _presenterSubscription; - /// /// Initializes static members of the class. /// diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index b85fe8461b..6bd1dd9162 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -56,6 +56,7 @@ + @@ -81,9 +82,7 @@ - - diff --git a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs index 65aa4ae4f9..1ea8b3ef41 100644 --- a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Layout; using Perspex.VisualTree; @@ -39,6 +42,7 @@ namespace Perspex.Controls.Presenters PerspexProperty.Register("CanScrollHorizontally", true); private Size _measuredExtent; + private IDisposable _scrollableSubscription; /// /// Initializes static members of the class. @@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + + GetObservable(ChildProperty).Subscribe(ChildChanged); } /// @@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - var content = Content as ILayoutable; + var child = Child; - if (content != null) + if (child != null) { - var measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + var measureSize = availableSize; - if (!CanScrollHorizontally) + if (_scrollableSubscription == null) { - measureSize = measureSize.WithWidth(availableSize.Width); + measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + + if (!CanScrollHorizontally) + { + measureSize = measureSize.WithWidth(availableSize.Width); + } } - content.Measure(measureSize); - var size = content.DesiredSize; + child.Measure(measureSize); + var size = child.DesiredSize; _measuredExtent = size; return size.Constrain(availableSize); } @@ -175,16 +186,21 @@ namespace Perspex.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; + var offset = default(Vector); - Viewport = finalSize; - Extent = _measuredExtent; + if (_scrollableSubscription == null) + { + Viewport = finalSize; + Extent = _measuredExtent; + offset = Offset; + } 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)); + child.Arrange(new Rect((Point)(-offset), size)); return finalSize; } @@ -209,6 +225,30 @@ namespace Perspex.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } + private void ChildChanged(IControl child) + { + var scrollable = child as IScrollable; + + _scrollableSubscription?.Dispose(); + _scrollableSubscription = null; + + if (scrollable != null) + { + scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); + _scrollableSubscription = new CompositeDisposable( + 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; + } + private static Vector ValidateOffset(ScrollContentPresenter o, Vector value) { return ScrollViewer.CoerceOffset( diff --git a/src/Perspex.Controls/Primitives/IScrollInfo.cs b/src/Perspex.Controls/Primitives/IScrollInfo.cs deleted file mode 100644 index 59dbc085e6..0000000000 --- a/src/Perspex.Controls/Primitives/IScrollInfo.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Perspex.Controls.Primitives -{ - public interface IScrollInfoBase - { - /// - /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant - /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo() - /// on this object when properties change. - /// - ScrollViewer ScrollOwner { get; set; } - - Rect MakeVisible(Visual visual, Rect rectangle); - } - - public interface IVerticalScrollInfo : IScrollInfoBase - { - /// - /// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible. - /// - double VerticalOffset { get; set; } - - /// - /// ExtentHeight contains the full vertical range of the scrolled content. - /// - double ExtentHeight { get; } - - /// - /// ViewportHeight contains the currently visible vertical range of the scrolled content. - /// - double ViewportHeight { get; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension. - /// - bool CanVerticallyScroll { get; set; } - - void LineDown(); - - void LineUp(); - - void MouseWheelDown(); - - void MouseWheelUp(); - - void PageDown(); - - void PageUp(); - } - - public interface IHorizontalScrollInfo : IScrollInfoBase - { - /// - /// ExtentWidth contains the full horizontal range of the scrolled content. - /// - double ExtentWidth { get; } - - /// - /// ViewportWidth contains the currently visible horizontal range of the scrolled content. - /// - double ViewportWidth { get; } - - /// - /// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible. - /// - double HorizontalOffset { get; set; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension. - /// - bool CanHorizontallyScroll { get; set; } - - void LineLeft(); - - void LineRight(); - - void MouseWheelLeft(); - - void MouseWheelRight(); - - void PageLeft(); - - void PageRight(); - } - - public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo - { - } -} diff --git a/src/Perspex.Controls/Primitives/IScrollable.cs b/src/Perspex.Controls/Primitives/IScrollable.cs new file mode 100644 index 0000000000..65adadb15e --- /dev/null +++ b/src/Perspex.Controls/Primitives/IScrollable.cs @@ -0,0 +1,39 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Perspex.Controls.Primitives +{ + /// + /// Interface implemented by controls that handle their own scrolling when placed inside a + /// . + /// + public interface IScrollable + { + /// + /// Gets or sets the invalidation method which notifies the attached + /// of a change in or . + /// + /// + /// This property is set by the parent when the + /// is placed inside it. + /// + Action InvalidateScroll { get; set; } + + /// + /// Gets the extent of the scrollable content, in logical units + /// + Size Extent { get; } + + /// + /// Gets or sets the current scroll offset, in logical units. + /// + Vector Offset { get; set; } + + /// + /// Gets the size of the viewport, in logical units. + /// + Size Viewport { get; } + } +} diff --git a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs b/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs deleted file mode 100644 index 8ea94f6fb6..0000000000 --- a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Perspex.Controls.Primitives -{ - public class ScrollInfoAdapter : IScrollInfo - { - private readonly IScrollInfoBase _nfo; - public ScrollInfoAdapter(IScrollInfoBase nfo) - { - _nfo = nfo; - } - - public ScrollViewer ScrollOwner - { - get { return _nfo.ScrollOwner; } - set { _nfo.ScrollOwner = value; } - } - - public double ExtentWidth => (_nfo as IHorizontalScrollInfo)?.ExtentWidth ?? 0; - - public double ViewportWidth => (_nfo as IHorizontalScrollInfo)?.ViewportWidth ?? 0; - - public double ExtentHeight => (_nfo as IVerticalScrollInfo)?.ExtentHeight ?? 0; - - public double ViewportHeight => (_nfo as IVerticalScrollInfo)?.ViewportHeight ?? 0; - - private double _horizontalOffset; - public double HorizontalOffset - { - get - { - return (_nfo as IHorizontalScrollInfo)?.HorizontalOffset ?? _horizontalOffset; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _horizontalOffset = value; - else - info.HorizontalOffset = value; - } - } - - private double _verticalOffset; - public double VerticalOffset - { - get - { - return (_nfo as IVerticalScrollInfo)?.VerticalOffset ?? _verticalOffset; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _verticalOffset = value; - else - info.VerticalOffset = value; - } - } - - public void LineLeft() => (_nfo as IHorizontalScrollInfo)?.LineLeft(); - - public void LineRight() => (_nfo as IHorizontalScrollInfo)?.LineRight(); - - public void MouseWheelLeft() => (_nfo as IHorizontalScrollInfo)?.MouseWheelLeft(); - - public void MouseWheelRight() => (_nfo as IHorizontalScrollInfo)?.MouseWheelRight(); - - public void PageLeft() => (_nfo as IHorizontalScrollInfo)?.PageLeft(); - - public Rect MakeVisible(Visual visual, Rect rectangle) => _nfo.MakeVisible(visual, rectangle); - - public void PageRight() => (_nfo as IHorizontalScrollInfo)?.PageRight(); - - public void LineDown() => (_nfo as IVerticalScrollInfo)?.LineDown(); - - public void LineUp() => (_nfo as IVerticalScrollInfo)?.LineUp(); - - public void MouseWheelDown() => (_nfo as IVerticalScrollInfo)?.MouseWheelDown(); - - public void MouseWheelUp() => (_nfo as IVerticalScrollInfo)?.MouseWheelUp(); - - public void PageDown() => (_nfo as IVerticalScrollInfo)?.PageDown(); - - public void PageUp() => (_nfo as IVerticalScrollInfo)?.PageUp(); - - private bool _canVerticallyScroll; - public bool CanVerticallyScroll - { - get - { - return (_nfo as IVerticalScrollInfo)?.CanVerticallyScroll ?? _canVerticallyScroll; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _canVerticallyScroll = value; - else - info.CanVerticallyScroll = value; - } - } - - private bool _canHorizontallyScroll; - public bool CanHorizontallyScroll - { - get - { - return (_nfo as IHorizontalScrollInfo)?.CanHorizontallyScroll ?? _canHorizontallyScroll; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _canHorizontallyScroll = value; - else - info.CanHorizontallyScroll = value; - } - } - } -} diff --git a/src/Perspex.Controls/ScrollViewer.cs b/src/Perspex.Controls/ScrollViewer.cs index 5af724a614..b454bc6d5e 100644 --- a/src/Perspex.Controls/ScrollViewer.cs +++ b/src/Perspex.Controls/ScrollViewer.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; @@ -117,6 +120,8 @@ namespace Perspex.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + private IDisposable _scrollableSubscription; + /// /// Initializes static members of the class. /// diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 19078b5b2e..2ec1569e11 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -93,6 +93,7 @@ + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index eb913b3410..adb4bfd0fa 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -142,6 +142,35 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); } + [Fact] + public void Measure_Should_Pass_Bounded_X_If_CannotScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + [ScrollContentPresenter.CanScrollHorizontallyProperty] = false, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, double.PositiveInfinity), child.AvailableSize); + } + + [Fact] + public void Measure_Should_Pass_Unbounded_X_If_CanScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(Size.Infinity, child.AvailableSize); + } + [Fact] public void Arrange_Should_Set_Viewport_And_Extent_In_That_Order() { @@ -240,8 +269,11 @@ namespace Perspex.Controls.UnitTests.Presenters private class TestControl : Control { + public Size AvailableSize { get; private set; } + protected override Size MeasureOverride(Size availableSize) { + AvailableSize = availableSize; return new Size(150, 150); } } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs new file mode 100644 index 0000000000..0dc1c31462 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -0,0 +1,216 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class ScrollContentPresenterTests_IScrollable + { + [Fact] + public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, 100), scrollable.AvailableSize); + } + + [Fact] + public void Arrange_Should_Not_Offset_IScrollable_Bounds() + { + 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.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + } + + [Fact] + public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable() + { + var target = new ScrollContentPresenter + { + Content = new TestScrollable() + }; + + var changed = false; + + target.Measure(new Size(100, 100)); + + target.GetObservable(ScrollViewer.ViewportProperty).Skip(1).Subscribe(_ => changed = true); + target.GetObservable(ScrollViewer.ExtentProperty).Skip(1).Subscribe(_ => changed = true); + + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.False(changed); + } + + [Fact] + public void InvalidateScroll_Should_Be_Set_When_Set_As_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + Assert.NotNull(scrollable.InvalidateScroll); + } + + [Fact] + public void InvalidateScroll_Should_Be_Cleared_When_Removed_From_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + target.Content = null; + target.ApplyTemplate(); + + Assert.Null(scrollable.InvalidateScroll); + } + + [Fact] + public void Extent_Offset_And_Viewport_Should_Be_Read_From_IScrollable() + { + 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.ApplyTemplate(); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + + scrollable.Extent = new Size(200, 200); + scrollable.Offset = new Vector(100, 100); + scrollable.Viewport = new Size(50, 50); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + } + + [Fact] + public void Offset_Should_Be_Written_To_IScrollable() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + target.Offset = new Vector(25, 25); + + Assert.Equal(target.Offset, scrollable.Offset); + } + + [Fact] + public void Offset_Should_Not_Be_Written_To_IScrollable_After_Removal() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.Content = null; + target.Offset = new Vector(25, 25); + + Assert.Equal(new Vector(50, 50), scrollable.Offset); + } + + private class TestScrollable : Control, IScrollable + { + private Size _extent; + private Vector _offset; + private Size _viewport; + + public Size AvailableSize { get; private set; } + public Action InvalidateScroll { get; set; } + + public Size Extent + { + get { return _extent; } + set + { + _extent = value; + InvalidateScroll?.Invoke(); + } + } + + public Vector Offset + { + get { return _offset; } + set + { + _offset = value; + InvalidateScroll?.Invoke(); + } + } + + public Size Viewport + { + get { return _viewport; } + set + { + _viewport = value; + InvalidateScroll?.Invoke(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + AvailableSize = availableSize; + return new Size(150, 150); + } + } + } +} \ No newline at end of file