Browse Source

Initial implementation of logical scrolling.

aka IScrollInfo in WPF.
pull/366/head
Steven Kirk 10 years ago
parent
commit
bfc010f757
  1. 92
      samples/XamlTestApplicationPcl/TestScrollable.cs
  2. 6
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  3. 1
      samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
  4. 2
      src/Perspex.Controls/ContentControl.cs
  5. 3
      src/Perspex.Controls/Perspex.Controls.csproj
  6. 60
      src/Perspex.Controls/Presenters/ScrollContentPresenter.cs
  7. 97
      src/Perspex.Controls/Primitives/IScrollInfo.cs
  8. 39
      src/Perspex.Controls/Primitives/IScrollable.cs
  9. 126
      src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs
  10. 5
      src/Perspex.Controls/ScrollViewer.cs
  11. 1
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  12. 32
      tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  13. 216
      tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs

92
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;
}
}
}
}
}

6
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -1,6 +1,7 @@
<Window x:Class="XamlTestApplication.MainWindow"
xmlns="https://github.com/perspex"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XamlTestApplication;assembly=XamlTestApplicationPcl"
xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplicationPcl"
Title="Perspex Test Application" Width="800" Height="600">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
@ -272,6 +273,11 @@
</Border>
</Grid>
</TabItem>
<TabItem Header="IScrollable">
<ScrollViewer>
<local:TestScrollable/>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Window>

1
samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj

@ -41,6 +41,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestScrollable.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
<Compile Include="ViewModels\TestNode.cs" />

2
src/Perspex.Controls/ContentControl.cs

@ -36,8 +36,6 @@ namespace Perspex.Controls
public static readonly PerspexProperty<VerticalAlignment> VerticalContentAlignmentProperty =
PerspexProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
private IDisposable _presenterSubscription;
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>

3
src/Perspex.Controls/Perspex.Controls.csproj

@ -56,6 +56,7 @@
<Compile Include="Platform\IWindowingPlatform.cs" />
<Compile Include="Platform\PlatformManager.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.cs" />
<Compile Include="Primitives\TabStripItem.cs" />
<Compile Include="Primitives\TemplateAppliedEventArgs.cs" />
<Compile Include="SelectionMode.cs" />
@ -81,9 +82,7 @@
<Compile Include="Platform\IPopupImpl.cs" />
<Compile Include="Platform\ITopLevelImpl.cs" />
<Compile Include="PlacementMode.cs" />
<Compile Include="Primitives\IScrollInfo.cs" />
<Compile Include="Primitives\Popup.cs" />
<Compile Include="Primitives\ScrollInfoAdapter.cs" />
<Compile Include="Canvas.cs" />
<Compile Include="Templates\FuncControlTemplate`2.cs" />
<Compile Include="Templates\FuncDataTemplate`1.cs" />

60
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<ScrollContentPresenter, bool>("CanScrollHorizontally", true);
private Size _measuredExtent;
private IDisposable _scrollableSubscription;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
GetObservable(ChildProperty).Subscribe(ChildChanged);
}
/// <summary>
@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters
/// <inheritdoc/>
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(

97
src/Perspex.Controls/Primitives/IScrollInfo.cs

@ -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
{
/// <summary>
/// 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.
/// </summary>
ScrollViewer ScrollOwner { get; set; }
Rect MakeVisible(Visual visual, Rect rectangle);
}
public interface IVerticalScrollInfo : IScrollInfoBase
{
/// <summary>
/// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible.
/// </summary>
double VerticalOffset { get; set; }
/// <summary>
/// ExtentHeight contains the full vertical range of the scrolled content.
/// </summary>
double ExtentHeight { get; }
/// <summary>
/// ViewportHeight contains the currently visible vertical range of the scrolled content.
/// </summary>
double ViewportHeight { get; }
/// <summary>
/// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension.
/// </summary>
bool CanVerticallyScroll { get; set; }
void LineDown();
void LineUp();
void MouseWheelDown();
void MouseWheelUp();
void PageDown();
void PageUp();
}
public interface IHorizontalScrollInfo : IScrollInfoBase
{
/// <summary>
/// ExtentWidth contains the full horizontal range of the scrolled content.
/// </summary>
double ExtentWidth { get; }
/// <summary>
/// ViewportWidth contains the currently visible horizontal range of the scrolled content.
/// </summary>
double ViewportWidth { get; }
/// <summary>
/// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible.
/// </summary>
double HorizontalOffset { get; set; }
/// <summary>
/// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension.
/// </summary>
bool CanHorizontallyScroll { get; set; }
void LineLeft();
void LineRight();
void MouseWheelLeft();
void MouseWheelRight();
void PageLeft();
void PageRight();
}
public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo
{
}
}

39
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
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets or sets the invalidation method which notifies the attached
/// <see cref="ScrollViewer"/> of a change in <see cref="Extent"/> or <see cref="Offset"/>.
/// </summary>
/// <remarks>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="IScrollable"/> is placed inside it.
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
}
}

126
src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs

@ -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;
}
}
}
}

5
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;
/// <summary>
/// Initializes static members of the <see cref="ScrollViewer"/> class.
/// </summary>

1
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -93,6 +93,7 @@
<Compile Include="ListBoxTests_Single.cs" />
<Compile Include="NameScopeTests.cs" />
<Compile Include="Primitives\SelectingItemsControlTests_Multiple.cs" />
<Compile Include="Presenters\ScrollContentPresenterTests_IScrollable.cs" />
<Compile Include="WindowingPlatformMock.cs" />
<Compile Include="TreeViewTests.cs" />
<Compile Include="Mixins\SelectableMixinTests.cs" />

32
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);
}
}

216
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);
}
}
}
}
Loading…
Cancel
Save