103 changed files with 4701 additions and 746 deletions
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<configuration> |
|||
<startup> |
|||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /> |
|||
</startup> |
|||
</configuration> |
|||
@ -0,0 +1,6 @@ |
|||
<Application xmlns="https://github.com/avaloniaui"> |
|||
<Application.Styles> |
|||
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/> |
|||
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -0,0 +1,16 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace VirtualizationTest |
|||
{ |
|||
public class App : Application |
|||
{ |
|||
public override void Initialize() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
Title="Avalonia Virtualization Test"> |
|||
<DockPanel LastChildFill="True" Margin="16"> |
|||
<StackPanel DockPanel.Dock="Right" |
|||
Margin="16 0 0 0" |
|||
MinWidth="150" |
|||
Gap="4"> |
|||
<DropDown Items="{Binding VirtualizationModes}" |
|||
SelectedItem="{Binding VirtualizationMode}"/> |
|||
<DropDown Items="{Binding Orientations}" |
|||
SelectedItem="{Binding Orientation}"/> |
|||
<TextBox Watermark="Item Count" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding ItemCount}"/> |
|||
<TextBox Watermark="Extent" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/> |
|||
<TextBox Watermark="Offset" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/> |
|||
<TextBox Watermark="Viewport" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/> |
|||
<TextBox Watermark="Item to Create" |
|||
UseFloatingWatermark="True" |
|||
Text="{Binding NewItemString}"/> |
|||
<Button Command="{Binding AddItemCommand}">Add Item</Button> |
|||
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button> |
|||
<Button Command="{Binding RecreateCommand}">Recreate</Button> |
|||
<Button Command="{Binding SelectFirstCommand}">Select First</Button> |
|||
<Button Command="{Binding SelectLastCommand}">Select Last</Button> |
|||
</StackPanel> |
|||
|
|||
<ListBox Name="listBox" |
|||
Items="{Binding Items}" |
|||
SelectedItems="{Binding SelectedItems}" |
|||
SelectionMode="Multiple" |
|||
VirtualizationMode="{Binding VirtualizationMode}"> |
|||
<ListBox.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<VirtualizingStackPanel Orientation="{Binding Orientation}"/> |
|||
</ItemsPanelTemplate> |
|||
</ListBox.ItemsPanel> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock Text="{Binding Header}"/> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</DockPanel> |
|||
</Window> |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using VirtualizationTest.ViewModels; |
|||
|
|||
namespace VirtualizationTest |
|||
{ |
|||
public class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
this.InitializeComponent(); |
|||
this.AttachDevTools(); |
|||
DataContext = new MainWindowViewModel(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Logging.Serilog; |
|||
using Serilog; |
|||
|
|||
namespace VirtualizationTest |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
InitializeLogging(); |
|||
|
|||
AppBuilder.Configure<App>() |
|||
.UseWin32() |
|||
.UseDirect2D1() |
|||
.Start<MainWindow>(); |
|||
} |
|||
|
|||
private static void InitializeLogging() |
|||
{ |
|||
#if DEBUG
|
|||
SerilogLogger.Initialize(new LoggerConfiguration() |
|||
.MinimumLevel.Warning() |
|||
.WriteTo.Trace(outputTemplate: "{Area}: {Message}") |
|||
.CreateLogger()); |
|||
#endif
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
// General Information about an assembly is controlled through the following
|
|||
// set of attributes. Change these attribute values to modify the information
|
|||
// associated with an assembly.
|
|||
[assembly: AssemblyTitle("VirtualizationTest")] |
|||
[assembly: AssemblyDescription("")] |
|||
[assembly: AssemblyConfiguration("")] |
|||
[assembly: AssemblyCompany("")] |
|||
[assembly: AssemblyProduct("VirtualizationTest")] |
|||
[assembly: AssemblyCopyright("Copyright © 2016")] |
|||
[assembly: AssemblyTrademark("")] |
|||
[assembly: AssemblyCulture("")] |
|||
|
|||
// Setting ComVisible to false makes the types in this assembly not visible
|
|||
// to COM components. If you need to access a type in this assembly from
|
|||
// COM, set the ComVisible attribute to true on that type.
|
|||
[assembly: ComVisible(false)] |
|||
|
|||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|||
[assembly: Guid("fbcaf3d0-2808-4934-8e96-3f607594517b")] |
|||
|
|||
// Version information for an assembly consists of the following four values:
|
|||
//
|
|||
// Major Version
|
|||
// Minor Version
|
|||
// Build Number
|
|||
// Revision
|
|||
//
|
|||
// You can specify all the values or you can default the Build and Revision Numbers
|
|||
// by using the '*' as shown below:
|
|||
// [assembly: AssemblyVersion("1.0.*")]
|
|||
[assembly: AssemblyVersion("1.0.0.0")] |
|||
[assembly: AssemblyFileVersion("1.0.0.0")] |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using ReactiveUI; |
|||
|
|||
namespace VirtualizationTest.ViewModels |
|||
{ |
|||
internal class ItemViewModel : ReactiveObject |
|||
{ |
|||
private string _prefix; |
|||
private int _index; |
|||
|
|||
public ItemViewModel(int index, string prefix = "Item") |
|||
{ |
|||
_prefix = prefix; |
|||
_index = index; |
|||
} |
|||
|
|||
public string Header => $"{_prefix} {_index}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
// Copyright (c) The Avalonia 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 Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using ReactiveUI; |
|||
|
|||
namespace VirtualizationTest.ViewModels |
|||
{ |
|||
internal class MainWindowViewModel : ReactiveObject |
|||
{ |
|||
private int _itemCount = 200; |
|||
private string _newItemString = "New Item"; |
|||
private int _newItemIndex; |
|||
private IReactiveList<ItemViewModel> _items; |
|||
private string _prefix = "Item"; |
|||
private Orientation _orientation; |
|||
private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; |
|||
|
|||
public MainWindowViewModel() |
|||
{ |
|||
this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); |
|||
RecreateCommand = ReactiveCommand.Create(); |
|||
RecreateCommand.Subscribe(_ => Recreate()); |
|||
|
|||
AddItemCommand = ReactiveCommand.Create(); |
|||
AddItemCommand.Subscribe(_ => AddItem()); |
|||
|
|||
RemoveItemCommand = ReactiveCommand.Create(); |
|||
RemoveItemCommand.Subscribe(_ => Remove()); |
|||
|
|||
SelectFirstCommand = ReactiveCommand.Create(); |
|||
SelectFirstCommand.Subscribe(_ => SelectItem(0)); |
|||
|
|||
SelectLastCommand = ReactiveCommand.Create(); |
|||
SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1)); |
|||
} |
|||
|
|||
public string NewItemString |
|||
{ |
|||
get { return _newItemString; } |
|||
set { this.RaiseAndSetIfChanged(ref _newItemString, value); } |
|||
} |
|||
|
|||
public int ItemCount |
|||
{ |
|||
get { return _itemCount; } |
|||
set { this.RaiseAndSetIfChanged(ref _itemCount, value); } |
|||
} |
|||
|
|||
public AvaloniaList<ItemViewModel> SelectedItems { get; } |
|||
= new AvaloniaList<ItemViewModel>(); |
|||
|
|||
public IReactiveList<ItemViewModel> Items |
|||
{ |
|||
get { return _items; } |
|||
private set { this.RaiseAndSetIfChanged(ref _items, value); } |
|||
} |
|||
|
|||
public Orientation Orientation |
|||
{ |
|||
get { return _orientation; } |
|||
set { this.RaiseAndSetIfChanged(ref _orientation, value); } |
|||
} |
|||
|
|||
public IEnumerable<Orientation> Orientations => |
|||
Enum.GetValues(typeof(Orientation)).Cast<Orientation>(); |
|||
|
|||
public ItemVirtualizationMode VirtualizationMode |
|||
{ |
|||
get { return _virtualizationMode; } |
|||
set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); } |
|||
} |
|||
|
|||
public IEnumerable<ItemVirtualizationMode> VirtualizationModes => |
|||
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>(); |
|||
|
|||
public ReactiveCommand<object> AddItemCommand { get; private set; } |
|||
public ReactiveCommand<object> RecreateCommand { get; private set; } |
|||
public ReactiveCommand<object> RemoveItemCommand { get; private set; } |
|||
public ReactiveCommand<object> SelectFirstCommand { get; private set; } |
|||
public ReactiveCommand<object> SelectLastCommand { get; private set; } |
|||
|
|||
private void ResizeItems(int count) |
|||
{ |
|||
if (Items == null) |
|||
{ |
|||
var items = Enumerable.Range(0, count) |
|||
.Select(x => new ItemViewModel(x)); |
|||
Items = new ReactiveList<ItemViewModel>(items); |
|||
} |
|||
else if (count > Items.Count) |
|||
{ |
|||
var items = Enumerable.Range(Items.Count, count - Items.Count) |
|||
.Select(x => new ItemViewModel(x)); |
|||
Items.AddRange(items); |
|||
} |
|||
else if (count < Items.Count) |
|||
{ |
|||
Items.RemoveRange(count, Items.Count - count); |
|||
} |
|||
} |
|||
|
|||
private void AddItem() |
|||
{ |
|||
var index = Items.Count; |
|||
|
|||
if (SelectedItems.Count > 0) |
|||
{ |
|||
index = Items.IndexOf(SelectedItems[0]); |
|||
} |
|||
|
|||
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); |
|||
} |
|||
|
|||
private void Remove() |
|||
{ |
|||
if (SelectedItems.Count > 0) |
|||
{ |
|||
Items.RemoveAll(SelectedItems); |
|||
} |
|||
} |
|||
|
|||
private void Recreate() |
|||
{ |
|||
_prefix = _prefix == "Item" ? "Recreated" : "Item"; |
|||
var items = Enumerable.Range(0, _itemCount) |
|||
.Select(x => new ItemViewModel(x, _prefix)); |
|||
Items = new ReactiveList<ItemViewModel>(items); |
|||
} |
|||
|
|||
private void SelectItem(int index) |
|||
{ |
|||
SelectedItems.Clear(); |
|||
SelectedItems.Add(Items[index]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> |
|||
<PropertyGroup> |
|||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
|||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> |
|||
<ProjectGuid>{FBCAF3D0-2808-4934-8E96-3F607594517B}</ProjectGuid> |
|||
<OutputType>WinExe</OutputType> |
|||
<AppDesignerFolder>Properties</AppDesignerFolder> |
|||
<RootNamespace>VirtualizationTest</RootNamespace> |
|||
<AssemblyName>VirtualizationTest</AssemblyName> |
|||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> |
|||
<FileAlignment>512</FileAlignment> |
|||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> |
|||
<PlatformTarget>AnyCPU</PlatformTarget> |
|||
<DebugSymbols>true</DebugSymbols> |
|||
<DebugType>full</DebugType> |
|||
<Optimize>false</Optimize> |
|||
<OutputPath>bin\Debug\</OutputPath> |
|||
<DefineConstants>DEBUG;TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> |
|||
<PlatformTarget>AnyCPU</PlatformTarget> |
|||
<DebugType>pdbonly</DebugType> |
|||
<Optimize>true</Optimize> |
|||
<OutputPath>bin\Release\</OutputPath> |
|||
<DefineConstants>TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<PropertyGroup> |
|||
<StartupObject /> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System" /> |
|||
<Reference Include="System.Core" /> |
|||
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="System.Xml.Linq" /> |
|||
<Reference Include="System.Data.DataSetExtensions" /> |
|||
<Reference Include="Microsoft.CSharp" /> |
|||
<Reference Include="System.Data" /> |
|||
<Reference Include="System.Net.Http" /> |
|||
<Reference Include="System.Xml" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Compile Include="App.xaml.cs"> |
|||
<DependentUpon>App.xaml</DependentUpon> |
|||
</Compile> |
|||
<Compile Include="MainWindow.xaml.cs"> |
|||
<DependentUpon>MainWindow.xaml</DependentUpon> |
|||
</Compile> |
|||
<Compile Include="Program.cs" /> |
|||
<Compile Include="Properties\AssemblyInfo.cs" /> |
|||
<Compile Include="ViewModels\ItemViewModel.cs" /> |
|||
<Compile Include="ViewModels\MainWindowViewModel.cs" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Include="App.config" /> |
|||
<None Include="packages.config" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj"> |
|||
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project> |
|||
<Name>Avalonia.Animation</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj"> |
|||
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project> |
|||
<Name>Avalonia.Base</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj"> |
|||
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project> |
|||
<Name>Avalonia.Controls</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj"> |
|||
<Project>{799a7bb5-3c2c-48b6-85a7-406a12c420da}</Project> |
|||
<Name>Avalonia.DesignerSupport</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj"> |
|||
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project> |
|||
<Name>Avalonia.Diagnostics</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj"> |
|||
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project> |
|||
<Name>Avalonia.Input</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj"> |
|||
<Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project> |
|||
<Name>Avalonia.Interactivity</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj"> |
|||
<Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project> |
|||
<Name>Avalonia.Layout</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Logging.Serilog\Avalonia.Logging.Serilog.csproj"> |
|||
<Project>{B61B66A3-B82D-4875-8001-89D3394FE0C9}</Project> |
|||
<Name>Avalonia.Logging.Serilog</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"> |
|||
<Project>{6417b24e-49c2-4985-8db2-3ab9d898ec91}</Project> |
|||
<Name>Avalonia.ReactiveUI</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.SceneGraph\Avalonia.SceneGraph.csproj"> |
|||
<Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project> |
|||
<Name>Avalonia.SceneGraph</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj"> |
|||
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project> |
|||
<Name>Avalonia.Styling</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj"> |
|||
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project> |
|||
<Name>Avalonia.Themes.Default</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj"> |
|||
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project> |
|||
<Name>Avalonia.Markup.Xaml</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj"> |
|||
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project> |
|||
<Name>Avalonia.Markup</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj"> |
|||
<Project>{3e908f67-5543-4879-a1dc-08eace79b3cd}</Project> |
|||
<Name>Avalonia.Direct2D1</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj"> |
|||
<Project>{811a76cf-1cf6-440f-963b-bbe31bd72a82}</Project> |
|||
<Name>Avalonia.Win32</Name> |
|||
</ProjectReference> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<EmbeddedResource Include="App.xaml"> |
|||
<SubType>Designer</SubType> |
|||
</EmbeddedResource> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<EmbeddedResource Include="MainWindow.xaml"> |
|||
<SubType>Designer</SubType> |
|||
</EmbeddedResource> |
|||
</ItemGroup> |
|||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> |
|||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. |
|||
Other similar extension points exist, see Microsoft.Common.targets. |
|||
<Target Name="BeforeBuild"> |
|||
</Target> |
|||
<Target Name="AfterBuild"> |
|||
</Target> |
|||
--> |
|||
</Project> |
|||
@ -0,0 +1,26 @@ |
|||
<ProjectConfiguration> |
|||
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies> |
|||
<BuildPriority>1000</BuildPriority> |
|||
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace> |
|||
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing> |
|||
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies> |
|||
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking> |
|||
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking> |
|||
<AllowCodeAnalysis>false</AllowCodeAnalysis> |
|||
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely> |
|||
<RunPreBuildEvents>false</RunPreBuildEvents> |
|||
<RunPostBuildEvents>false</RunPostBuildEvents> |
|||
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully> |
|||
<InstrumentAssembly>true</InstrumentAssembly> |
|||
<PreventSigningOfAssembly>false</PreventSigningOfAssembly> |
|||
<AnalyseExecutionTimes>true</AnalyseExecutionTimes> |
|||
<DetectStackOverflow>true</DetectStackOverflow> |
|||
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace> |
|||
<DefaultTestTimeout>60000</DefaultTestTimeout> |
|||
<UseBuildConfiguration></UseBuildConfiguration> |
|||
<UseBuildPlatform></UseBuildPlatform> |
|||
<ProxyProcessPath></ProxyProcessPath> |
|||
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture> |
|||
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState> |
|||
<BuildProcessArchitecture>x86</BuildProcessArchitecture> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,10 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<packages> |
|||
<package id="Rx-Core" version="2.2.5" targetFramework="net452" /> |
|||
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net452" /> |
|||
<package id="Rx-Linq" version="2.2.5" targetFramework="net452" /> |
|||
<package id="Rx-Main" version="2.2.5" targetFramework="net452" /> |
|||
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net452" /> |
|||
<package id="Serilog" version="1.5.14" targetFramework="net452" /> |
|||
<package id="Splat" version="1.6.2" targetFramework="net452" /> |
|||
</packages> |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Controls.Generators |
|||
{ |
|||
public class MenuItemContainerGenerator : ItemContainerGenerator<MenuItem> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner control.</param>
|
|||
public MenuItemContainerGenerator(IControl owner) |
|||
: base(owner, MenuItem.HeaderProperty, null) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override IControl CreateContainer(object item) |
|||
{ |
|||
var separator = item as Separator; |
|||
return separator != null ? separator : base.CreateContainer(item); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Interface implemented by scrollable controls.
|
|||
/// </summary>
|
|||
public interface IScrollable |
|||
{ |
|||
/// <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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines an interface through which a <see cref="Control"/>'s inheritance parent can be set.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// You should not usually need to use this interface - it is for advanced scenarios only.
|
|||
/// Additionally, <see cref="ISetLogicalParent"/> also sets the inheritance parent; this
|
|||
/// interface is only needed where the logical and inheritance parents differ.
|
|||
/// </remarks>
|
|||
public interface ISetInheritanceParent |
|||
{ |
|||
/// <summary>
|
|||
/// Sets the control's inheritance parent.
|
|||
/// </summary>
|
|||
/// <param name="parent">The parent.</param>
|
|||
void SetParent(IAvaloniaObject parent); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Interface implemented by controls that act as controllers for an
|
|||
/// <see cref="IVirtualizingPanel"/>.
|
|||
/// </summary>
|
|||
public interface IVirtualizingController |
|||
{ |
|||
/// <summary>
|
|||
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The controller should respond to this method being called by either adding
|
|||
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
|
|||
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
|
|||
/// </remarks>
|
|||
void UpdateControls(); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// A panel that can be used to virtualize items.
|
|||
/// </summary>
|
|||
public interface IVirtualizingPanel : IPanel |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the controller for the virtualizing panel.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A virtualizing controller is responsible for maintaing the controls in the virtualizing
|
|||
/// panel. This property will be set by the controller when virtualization is initialized.
|
|||
/// Note that this property may remain null if the panel is added to a control that does
|
|||
/// not act as a virtualizing controller.
|
|||
/// </remarks>
|
|||
IVirtualizingController Controller { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the panel is full.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should return false until enough children are added to fill the space
|
|||
/// passed into the last measure in the direction of scroll. It should be updated
|
|||
/// immediately after a child is added or removed.
|
|||
/// </remarks>
|
|||
bool IsFull { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items that can be removed while keeping the panel full.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This property should return the number of children that are completely out of the
|
|||
/// panel's current bounds in the direction of scroll. It should be updated after an
|
|||
/// arrange.
|
|||
/// </remarks>
|
|||
int OverflowCount { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the direction of scroll.
|
|||
/// </summary>
|
|||
Orientation ScrollDirection { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the average size of the materialized items in the direction of scroll.
|
|||
/// </summary>
|
|||
double AverageItemSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
|
|||
/// direction of scroll.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
|
|||
/// overflows the panel bounds.
|
|||
/// </remarks>
|
|||
double PixelOverflow { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current pixel offset of the items in the direction of scroll.
|
|||
/// </summary>
|
|||
double PixelOffset { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Describes the item virtualization method to use for a list.
|
|||
/// </summary>
|
|||
public enum ItemVirtualizationMode |
|||
{ |
|||
/// <summary>
|
|||
/// Do not virtualize items.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Virtualize items without smooth scrolling.
|
|||
/// </summary>
|
|||
Simple, |
|||
} |
|||
} |
|||
@ -0,0 +1,198 @@ |
|||
// Copyright (c) The Avalonia 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; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Input; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
|
|||
/// </summary>
|
|||
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable |
|||
{ |
|||
private bool disposedValue; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner"></param>
|
|||
public ItemVirtualizer(ItemsPresenter owner) |
|||
{ |
|||
Owner = owner; |
|||
Items = owner.Items; |
|||
ItemCount = owner.Items.Count(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ItemsPresenter"/> which owns the virtualizer.
|
|||
/// </summary>
|
|||
public ItemsPresenter Owner { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IVirtualizingPanel"/> which will host the items.
|
|||
/// </summary>
|
|||
public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; |
|||
|
|||
/// <summary>
|
|||
/// Gets the items to display.
|
|||
/// </summary>
|
|||
public IEnumerable Items { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items in <see cref="Items"/>.
|
|||
/// </summary>
|
|||
public int ItemCount { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the index of the first item displayed in the panel.
|
|||
/// </summary>
|
|||
public int FirstIndex { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the index of the first item beyond those displayed in the panel.
|
|||
/// </summary>
|
|||
public int NextIndex { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
|
|||
/// </summary>
|
|||
public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether logical scrolling is enabled.
|
|||
/// </summary>
|
|||
public abstract bool IsLogicalScrollEnabled { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the scroll extent.
|
|||
/// </summary>
|
|||
public abstract double ExtentValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the value of the current scroll offset.
|
|||
/// </summary>
|
|||
public abstract double OffsetValue { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the scrollable viewport.
|
|||
/// </summary>
|
|||
public abstract double ViewportValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
|
|||
/// </summary>
|
|||
public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
|
|||
/// </summary>
|
|||
public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
|
|||
/// </summary>
|
|||
public Vector Offset |
|||
{ |
|||
get |
|||
{ |
|||
return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); |
|||
} |
|||
|
|||
set |
|||
{ |
|||
OffsetValue = Vertical ? value.Y : value.X; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates an <see cref="ItemVirtualizer"/> based on an item presenter's
|
|||
/// <see cref="ItemVirtualizationMode"/>.
|
|||
/// </summary>
|
|||
/// <param name="owner">The items presenter.</param>
|
|||
/// <returns>An <see cref="ItemVirtualizer"/>.</returns>
|
|||
public static ItemVirtualizer Create(ItemsPresenter owner) |
|||
{ |
|||
var virtualizingPanel = owner.Panel as IVirtualizingPanel; |
|||
var scrollable = (ILogicalScrollable)owner; |
|||
ItemVirtualizer result = null; |
|||
|
|||
if (virtualizingPanel != null && scrollable.InvalidateScroll != null) |
|||
{ |
|||
switch (owner.VirtualizationMode) |
|||
{ |
|||
case ItemVirtualizationMode.Simple: |
|||
result = new ItemVirtualizerSimple(owner); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (result == null) |
|||
{ |
|||
result = new ItemVirtualizerNone(owner); |
|||
} |
|||
|
|||
if (virtualizingPanel != null) |
|||
{ |
|||
virtualizingPanel.Controller = result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void UpdateControls() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the next control in the specified direction.
|
|||
/// </summary>
|
|||
/// <param name="direction">The movement direction.</param>
|
|||
/// <param name="from">The control from which movement begins.</param>
|
|||
/// <returns>The control.</returns>
|
|||
public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the items for the presenter change, either because
|
|||
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
|
|||
/// modified, or the panel has been created.
|
|||
/// </summary>
|
|||
/// <param name="items">The items.</param>
|
|||
/// <param name="e">A description of the change.</param>
|
|||
public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
Items = items; |
|||
ItemCount = items.Count(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the specified item into view.
|
|||
/// </summary>
|
|||
/// <param name="item">The item.</param>
|
|||
public virtual void ScrollIntoView(object item) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public virtual void Dispose() |
|||
{ |
|||
VirtualizingPanel.Controller = null; |
|||
VirtualizingPanel.Children.Clear(); |
|||
Owner.ItemContainerGenerator.Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invalidates the current scroll.
|
|||
/// </summary>
|
|||
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
// Copyright (c) The Avalonia 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; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an item virtualizer for an <see cref="ItemsPresenter"/> that doesn't actually
|
|||
/// virtualize items - it just creates a container for every item.
|
|||
/// </summary>
|
|||
internal class ItemVirtualizerNone : ItemVirtualizer |
|||
{ |
|||
public ItemVirtualizerNone(ItemsPresenter owner) |
|||
: base(owner) |
|||
{ |
|||
if (Items != null && owner.Panel != null) |
|||
{ |
|||
AddContainers(0, Items); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool IsLogicalScrollEnabled => false; |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double ExtentValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double OffsetValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
set { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
|
|||
/// false.
|
|||
/// </summary>
|
|||
public override double ViewportValue |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ItemsChanged(items, e); |
|||
|
|||
var generator = Owner.ItemContainerGenerator; |
|||
var panel = Owner.Panel; |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
if (e.NewStartingIndex + e.NewItems.Count < Items.Count()) |
|||
{ |
|||
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); |
|||
} |
|||
|
|||
AddContainers(e.NewStartingIndex, e.NewItems); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); |
|||
var containers = AddContainers(e.NewStartingIndex, e.NewItems); |
|||
|
|||
var i = e.NewStartingIndex; |
|||
|
|||
foreach (var container in containers) |
|||
{ |
|||
panel.Children[i++] = container.ContainerControl; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Move: |
|||
// TODO: Handle move in a more efficient manner. At the moment we just
|
|||
// drop through to Reset to recreate all the containers.
|
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
RemoveContainers(generator.Clear()); |
|||
|
|||
if (Items != null) |
|||
{ |
|||
AddContainers(0, Items); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
Owner.InvalidateMeasure(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the specified item into view.
|
|||
/// </summary>
|
|||
/// <param name="item">The item.</param>
|
|||
public override void ScrollIntoView(object item) |
|||
{ |
|||
var index = Items.IndexOf(item); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); |
|||
container.BringIntoView(); |
|||
} |
|||
} |
|||
|
|||
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items) |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var result = new List<ItemContainerInfo>(); |
|||
var panel = Owner.Panel; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
var i = generator.Materialize(index++, item, Owner.MemberSelector); |
|||
|
|||
if (i.ContainerControl != null) |
|||
{ |
|||
if (i.Index < panel.Children.Count) |
|||
{ |
|||
// TODO: This will insert at the wrong place when there are null items.
|
|||
panel.Children.Insert(i.Index, i.ContainerControl); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.Add(i.ContainerControl); |
|||
} |
|||
} |
|||
|
|||
result.Add(i); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private void RemoveContainers(IEnumerable<ItemContainerInfo> items) |
|||
{ |
|||
var panel = Owner.Panel; |
|||
|
|||
foreach (var i in items) |
|||
{ |
|||
if (i.ContainerControl != null) |
|||
{ |
|||
panel.Children.Remove(i.ContainerControl); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,491 @@ |
|||
// Copyright (c) The Avalonia 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; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls.Presenters |
|||
{ |
|||
/// <summary>
|
|||
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
|
|||
/// <see cref="ItemVirtualizationMode.Simple"/>.
|
|||
/// </summary>
|
|||
internal class ItemVirtualizerSimple : ItemVirtualizer |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner"></param>
|
|||
public ItemVirtualizerSimple(ItemsPresenter owner) |
|||
: base(owner) |
|||
{ |
|||
// Don't need to add children here as UpdateControls should be called by the panel
|
|||
// measure/arrange.
|
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool IsLogicalScrollEnabled => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double ExtentValue => ItemCount; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double OffsetValue |
|||
{ |
|||
get |
|||
{ |
|||
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; |
|||
return FirstIndex + offset; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; |
|||
var delta = (int)(value - (FirstIndex + offset)); |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
var newLastIndex = (NextIndex - 1) + delta; |
|||
|
|||
if (newLastIndex < ItemCount) |
|||
{ |
|||
if (panel.PixelOffset > 0) |
|||
{ |
|||
panel.PixelOffset = 0; |
|||
delta += 1; |
|||
} |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
RecycleContainersForMove(delta); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// We're moving to a partially obscured item at the end of the list so
|
|||
// offset the panel by the height of the first item.
|
|||
var firstIndex = ItemCount - panel.Children.Count; |
|||
RecycleContainersForMove(firstIndex - FirstIndex); |
|||
|
|||
panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? |
|||
panel.Children[0].Bounds.Height : |
|||
panel.Children[0].Bounds.Width; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double ViewportValue |
|||
{ |
|||
get |
|||
{ |
|||
// If we can't fit the last item in the panel fully, subtract 1 from the viewport.
|
|||
var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; |
|||
return VirtualizingPanel.Children.Count - overflow; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void UpdateControls() |
|||
{ |
|||
CreateAndRemoveContainers(); |
|||
InvalidateScroll(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ItemsChanged(items, e); |
|||
|
|||
if (items != null) |
|||
{ |
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
if (e.NewStartingIndex >= FirstIndex && |
|||
e.NewStartingIndex + e.NewItems.Count <= NextIndex) |
|||
{ |
|||
CreateAndRemoveContainers(); |
|||
RecycleContainers(); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
if (e.OldStartingIndex >= FirstIndex && |
|||
e.OldStartingIndex + e.OldItems.Count <= NextIndex) |
|||
{ |
|||
RecycleContainersOnRemove(); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Move: |
|||
case NotifyCollectionChangedAction.Replace: |
|||
RecycleContainers(); |
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
RecycleContainersOnRemove(); |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Owner.ItemContainerGenerator.Clear(); |
|||
VirtualizingPanel.Children.Clear(); |
|||
} |
|||
|
|||
InvalidateScroll(); |
|||
} |
|||
|
|||
public override IControl GetControlInDirection(NavigationDirection direction, IControl from) |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var panel = VirtualizingPanel; |
|||
var itemIndex = generator.IndexFromContainer(from); |
|||
var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; |
|||
|
|||
if (itemIndex == -1) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var newItemIndex = -1; |
|||
|
|||
switch (direction) |
|||
{ |
|||
case NavigationDirection.First: |
|||
newItemIndex = 0; |
|||
break; |
|||
|
|||
case NavigationDirection.Last: |
|||
newItemIndex = ItemCount - 1; |
|||
break; |
|||
|
|||
case NavigationDirection.Up: |
|||
if (vertical) |
|||
{ |
|||
newItemIndex = itemIndex - 1; |
|||
} |
|||
|
|||
break; |
|||
case NavigationDirection.Down: |
|||
if (vertical) |
|||
{ |
|||
newItemIndex = itemIndex + 1; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NavigationDirection.Left: |
|||
if (!vertical) |
|||
{ |
|||
newItemIndex = itemIndex - 1; |
|||
} |
|||
break; |
|||
|
|||
case NavigationDirection.Right: |
|||
if (!vertical) |
|||
{ |
|||
newItemIndex = itemIndex + 1; |
|||
} |
|||
break; |
|||
|
|||
case NavigationDirection.PageUp: |
|||
newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); |
|||
break; |
|||
|
|||
case NavigationDirection.PageDown: |
|||
newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); |
|||
break; |
|||
} |
|||
|
|||
return ScrollIntoView(newItemIndex); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ScrollIntoView(object item) |
|||
{ |
|||
var index = Items.IndexOf(item); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
ScrollIntoView(index); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates and removes containers such that we have at most enough containers to fill
|
|||
/// the panel.
|
|||
/// </summary>
|
|||
private void CreateAndRemoveContainers() |
|||
{ |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var panel = VirtualizingPanel; |
|||
|
|||
if (!panel.IsFull && Items != null) |
|||
{ |
|||
var memberSelector = Owner.MemberSelector; |
|||
var index = NextIndex; |
|||
var step = 1; |
|||
|
|||
while (!panel.IsFull) |
|||
{ |
|||
if (index >= ItemCount) |
|||
{ |
|||
// We can fit more containers in the panel, but we're at the end of the
|
|||
// items. If we're scrolled to the top (FirstIndex == 0), then there are
|
|||
// no more items to create. Otherwise, go backwards adding containers to
|
|||
// the beginning of the panel.
|
|||
if (FirstIndex == 0) |
|||
{ |
|||
break; |
|||
} |
|||
else |
|||
{ |
|||
index = FirstIndex - 1; |
|||
step = -1; |
|||
} |
|||
} |
|||
|
|||
var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); |
|||
|
|||
if (step == 1) |
|||
{ |
|||
panel.Children.Add(materialized.ContainerControl); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.Insert(0, materialized.ContainerControl); |
|||
} |
|||
|
|||
index += step; |
|||
} |
|||
|
|||
if (step == 1) |
|||
{ |
|||
NextIndex = index; |
|||
} |
|||
else |
|||
{ |
|||
NextIndex = ItemCount; |
|||
FirstIndex = index + 1; |
|||
} |
|||
} |
|||
|
|||
if (panel.OverflowCount > 0) |
|||
{ |
|||
RemoveContainers(panel.OverflowCount); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the containers in the panel to make sure they are displaying the correct item
|
|||
/// based on <see cref="ItemVirtualizer.FirstIndex"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
|
|||
/// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
|
|||
/// </remarks>
|
|||
private void RecycleContainers() |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var selector = Owner.MemberSelector; |
|||
var containers = generator.Containers.ToList(); |
|||
var itemIndex = FirstIndex; |
|||
|
|||
foreach (var container in containers) |
|||
{ |
|||
var item = Items.ElementAt(itemIndex); |
|||
|
|||
if (!object.Equals(container.Item, item)) |
|||
{ |
|||
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
++itemIndex; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles containers when a move occurs.
|
|||
/// </summary>
|
|||
/// <param name="delta">The delta of the move.</param>
|
|||
/// <remarks>
|
|||
/// If the move is less than a page, then this method moves the containers for the items
|
|||
/// that are still visible to the correct place, and recyles and moves the others. For
|
|||
/// example: if there are 20 items and 10 containers visible and the user scrolls 5
|
|||
/// items down, then the bottom 5 containers will be moved to the top and the top 5 will
|
|||
/// be moved to the bottom and recycled to display the newly visible item. Updates
|
|||
/// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
|
|||
/// with their new values.
|
|||
/// </remarks>
|
|||
private void RecycleContainersForMove(int delta) |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var selector = Owner.MemberSelector; |
|||
var sign = delta < 0 ? -1 : 1; |
|||
var count = Math.Min(Math.Abs(delta), panel.Children.Count); |
|||
var move = count < panel.Children.Count; |
|||
var first = delta < 0 && move ? panel.Children.Count + delta : 0; |
|||
var containers = panel.Children.GetRange(first, count).ToList(); |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var oldItemIndex = FirstIndex + first + i; |
|||
var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); |
|||
|
|||
var item = Items.ElementAt(newItemIndex); |
|||
|
|||
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
if (move) |
|||
{ |
|||
if (delta > 0) |
|||
{ |
|||
panel.Children.MoveRange(first, count, panel.Children.Count); |
|||
} |
|||
else |
|||
{ |
|||
panel.Children.MoveRange(first, count, 0); |
|||
} |
|||
} |
|||
|
|||
FirstIndex += delta; |
|||
NextIndex += delta; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Recycles containers due to items being removed.
|
|||
/// </summary>
|
|||
private void RecycleContainersOnRemove() |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
|
|||
if (NextIndex <= ItemCount) |
|||
{ |
|||
// Items have been removed but FirstIndex..NextIndex is still a valid range in the
|
|||
// items, so just recycle the containers to adapt to the new state.
|
|||
RecycleContainers(); |
|||
} |
|||
else |
|||
{ |
|||
// Items have been removed and now the range FirstIndex..NextIndex goes out of
|
|||
// the item bounds. Remove any excess containers, try to scroll up and then recycle
|
|||
// the containers to make sure they point to the correct item.
|
|||
var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); |
|||
var delta = newFirstIndex - FirstIndex; |
|||
var newNextIndex = NextIndex + delta; |
|||
|
|||
if (newNextIndex > ItemCount) |
|||
{ |
|||
RemoveContainers(newNextIndex - ItemCount); |
|||
} |
|||
|
|||
if (delta != 0) |
|||
{ |
|||
RecycleContainersForMove(delta); |
|||
} |
|||
|
|||
RecycleContainers(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the specified number of containers from the end of the panel and updates
|
|||
/// <see cref="ItemVirtualizer.NextIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="count">The number of containers to remove.</param>
|
|||
private void RemoveContainers(int count) |
|||
{ |
|||
var index = VirtualizingPanel.Children.Count - count; |
|||
|
|||
VirtualizingPanel.Children.RemoveRange(index, count); |
|||
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); |
|||
NextIndex -= count; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scrolls the item with the specified index into view.
|
|||
/// </summary>
|
|||
/// <param name="index">The item index.</param>
|
|||
/// <returns>The container that was brought into view.</returns>
|
|||
private IControl ScrollIntoView(int index) |
|||
{ |
|||
var panel = VirtualizingPanel; |
|||
var generator = Owner.ItemContainerGenerator; |
|||
var newOffset = -1.0; |
|||
|
|||
if (index >= 0 && index < ItemCount) |
|||
{ |
|||
if (index < FirstIndex) |
|||
{ |
|||
newOffset = index; |
|||
} |
|||
else if (index >= NextIndex) |
|||
{ |
|||
newOffset = index - Math.Ceiling(ViewportValue - 1); |
|||
} |
|||
else if (OffsetValue + ViewportValue >= ItemCount) |
|||
{ |
|||
newOffset = OffsetValue - 1; |
|||
} |
|||
|
|||
if (newOffset != -1) |
|||
{ |
|||
OffsetValue = newOffset; |
|||
} |
|||
|
|||
var container = generator.ContainerFromIndex(index); |
|||
var layoutManager = LayoutManager.Instance; |
|||
|
|||
// We need to do a layout here because it's possible that the container we moved to
|
|||
// is only partially visible due to differing item sizes. If the container is only
|
|||
// partially visible, scroll again. Don't do this if there's no layout manager:
|
|||
// it means we're running a unit test.
|
|||
if (layoutManager != null) |
|||
{ |
|||
layoutManager.ExecuteLayoutPass(); |
|||
|
|||
if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) |
|||
{ |
|||
OffsetValue += 1; |
|||
} |
|||
} |
|||
|
|||
return container; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ensures an offset value is within the value range.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
/// <returns>The coerced value.</returns>
|
|||
private double CoerceOffset(double value) |
|||
{ |
|||
var max = Math.Max(ExtentValue - ViewportValue, 0); |
|||
return MathUtilities.Clamp(value, 0, max); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Interface implemented by controls that handle their own scrolling when placed inside a
|
|||
/// <see cref="ScrollViewer"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Controls that implement this interface, when placed inside a <see cref="ScrollViewer"/>
|
|||
/// 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.
|
|||
/// </remarks>
|
|||
public interface ILogicalScrollable : IScrollable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a value indicating whether logical scrolling is enabled on the control.
|
|||
/// </summary>
|
|||
bool IsLogicalScrollEnabled { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the scroll invalidation method.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
|
|||
/// the <see cref="IScrollable.Extent"/>, <see cref="IScrollable.Offset"/> or
|
|||
/// <see cref="IScrollable.Viewport"/> properties.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// This property is set by the parent <see cref="ScrollViewer"/> when the
|
|||
/// <see cref="ILogicalScrollable"/> is placed inside it.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
Action InvalidateScroll { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size to scroll by, in logical units.
|
|||
/// </summary>
|
|||
Size ScrollSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size to page by, in logical units.
|
|||
/// </summary>
|
|||
Size PageScrollSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Attempts to bring a portion of the target visual into view by scrolling the content.
|
|||
/// </summary>
|
|||
/// <param name="target">The target visual.</param>
|
|||
/// <param name="targetRect">The portion of the target visual to bring into view.</param>
|
|||
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
|
|||
bool BringIntoView(IControl target, Rect targetRect); |
|||
|
|||
/// <summary>
|
|||
/// Gets the next control in the specified direction.
|
|||
/// </summary>
|
|||
/// <param name="direction">The movement direction.</param>
|
|||
/// <param name="from">The control from which movement begins.</param>
|
|||
/// <returns>The control.</returns>
|
|||
IControl GetControlInDirection(NavigationDirection direction, IControl from); |
|||
} |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
|
|||
namespace Avalonia.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 scroll invalidation method.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
|
|||
/// the <see cref="Extent"/>, <see cref="Offset"/> or <see cref="Viewport"/> properties.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// This property is set by the parent <see cref="ScrollViewer"/> when the
|
|||
/// <see cref="IScrollable"/> is placed inside it.
|
|||
/// </para>
|
|||
/// </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; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size to scroll by, in logical units.
|
|||
/// </summary>
|
|||
Size ScrollSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size to page by, in logical units.
|
|||
/// </summary>
|
|||
Size PageScrollSize { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,229 @@ |
|||
// Copyright (c) The Avalonia 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.Specialized; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel |
|||
{ |
|||
private Size _availableSpace; |
|||
private double _takenSpace; |
|||
private int _canBeRemoved; |
|||
private double _averageItemSize; |
|||
private int _averageCount; |
|||
private double _pixelOffset; |
|||
|
|||
bool IVirtualizingPanel.IsFull |
|||
{ |
|||
get |
|||
{ |
|||
return Orientation == Orientation.Horizontal ? |
|||
_takenSpace >= _availableSpace.Width : |
|||
_takenSpace >= _availableSpace.Height; |
|||
} |
|||
} |
|||
|
|||
IVirtualizingController IVirtualizingPanel.Controller { get; set; } |
|||
int IVirtualizingPanel.OverflowCount => _canBeRemoved; |
|||
Orientation IVirtualizingPanel.ScrollDirection => Orientation; |
|||
double IVirtualizingPanel.AverageItemSize => _averageItemSize; |
|||
|
|||
double IVirtualizingPanel.PixelOverflow |
|||
{ |
|||
get |
|||
{ |
|||
var bounds = Orientation == Orientation.Horizontal ? |
|||
_availableSpace.Width : _availableSpace.Height; |
|||
return Math.Max(0, _takenSpace - bounds); |
|||
} |
|||
} |
|||
|
|||
double IVirtualizingPanel.PixelOffset |
|||
{ |
|||
get { return _pixelOffset; } |
|||
|
|||
set |
|||
{ |
|||
if (_pixelOffset != value) |
|||
{ |
|||
_pixelOffset = value; |
|||
InvalidateArrange(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
if (availableSize != ((ILayoutable)this).PreviousMeasure) |
|||
{ |
|||
// TODO: We need to put a reasonable limit on this, probably based on the max
|
|||
// window size.
|
|||
_availableSpace = availableSize; |
|||
Controller?.UpdateControls(); |
|||
} |
|||
|
|||
return base.MeasureOverride(availableSize); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
_availableSpace = finalSize; |
|||
_canBeRemoved = 0; |
|||
_takenSpace = 0; |
|||
_averageItemSize = 0; |
|||
_averageCount = 0; |
|||
var result = base.ArrangeOverride(finalSize); |
|||
_takenSpace += _pixelOffset; |
|||
Controller?.UpdateControls(); |
|||
return result; |
|||
} |
|||
|
|||
protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
base.ChildrenChanged(sender, e); |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
foreach (IControl control in e.NewItems) |
|||
{ |
|||
UpdateAdd(control); |
|||
} |
|||
|
|||
break; |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
foreach (IControl control in e.OldItems) |
|||
{ |
|||
UpdateRemove(control); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) |
|||
{ |
|||
var logicalScrollable = Parent as ILogicalScrollable; |
|||
var fromControl = from as IControl; |
|||
|
|||
if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) |
|||
{ |
|||
return logicalScrollable.GetControlInDirection(direction, fromControl); |
|||
} |
|||
else |
|||
{ |
|||
return base.GetControlInDirection(direction, from); |
|||
} |
|||
} |
|||
|
|||
internal override void ArrangeChild( |
|||
IControl child, |
|||
Rect rect, |
|||
Size panelSize, |
|||
Orientation orientation) |
|||
{ |
|||
if (orientation == Orientation.Vertical) |
|||
{ |
|||
rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); |
|||
child.Arrange(rect); |
|||
|
|||
if (rect.Y >= _availableSpace.Height) |
|||
{ |
|||
++_canBeRemoved; |
|||
} |
|||
|
|||
if (rect.Bottom >= _takenSpace) |
|||
{ |
|||
_takenSpace = rect.Bottom; |
|||
} |
|||
|
|||
AddToAverageItemSize(rect.Height); |
|||
} |
|||
else |
|||
{ |
|||
rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); |
|||
child.Arrange(rect); |
|||
|
|||
if (rect.X >= _availableSpace.Width) |
|||
{ |
|||
++_canBeRemoved; |
|||
} |
|||
|
|||
if (rect.Right >= _takenSpace) |
|||
{ |
|||
_takenSpace = rect.Right; |
|||
} |
|||
|
|||
AddToAverageItemSize(rect.Width); |
|||
} |
|||
} |
|||
|
|||
private void UpdateAdd(IControl child) |
|||
{ |
|||
var bounds = Bounds; |
|||
var gap = Gap; |
|||
|
|||
child.Measure(_availableSpace); |
|||
++_averageCount; |
|||
|
|||
if (Orientation == Orientation.Vertical) |
|||
{ |
|||
var height = child.DesiredSize.Height; |
|||
_takenSpace += height + gap; |
|||
AddToAverageItemSize(height); |
|||
} |
|||
else |
|||
{ |
|||
var width = child.DesiredSize.Width; |
|||
_takenSpace += width + gap; |
|||
AddToAverageItemSize(width); |
|||
} |
|||
} |
|||
|
|||
private void UpdateRemove(IControl child) |
|||
{ |
|||
var bounds = Bounds; |
|||
var gap = Gap; |
|||
|
|||
if (Orientation == Orientation.Vertical) |
|||
{ |
|||
var height = child.DesiredSize.Height; |
|||
_takenSpace -= height + gap; |
|||
RemoveFromAverageItemSize(height); |
|||
} |
|||
else |
|||
{ |
|||
var width = child.DesiredSize.Width; |
|||
_takenSpace -= width + gap; |
|||
RemoveFromAverageItemSize(width); |
|||
} |
|||
|
|||
if (_canBeRemoved > 0) |
|||
{ |
|||
--_canBeRemoved; |
|||
} |
|||
} |
|||
|
|||
private void AddToAverageItemSize(double value) |
|||
{ |
|||
++_averageCount; |
|||
_averageItemSize += (value - _averageItemSize) / _averageCount; |
|||
} |
|||
|
|||
private void RemoveFromAverageItemSize(double value) |
|||
{ |
|||
_averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1); |
|||
--_averageCount; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Layout; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests.Presenters |
|||
{ |
|||
public class ItemsPresenterTests_Virtualization |
|||
{ |
|||
[Fact] |
|||
public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() |
|||
{ |
|||
var target = CreateTarget(); |
|||
target.ClearValue(ItemsPresenter.ItemsPanelProperty); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() |
|||
{ |
|||
var target = CreateTarget(ItemVirtualizationMode.None); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent() |
|||
{ |
|||
var target = new ItemsPresenter |
|||
{ |
|||
ItemsPanel = VirtualizingPanelTemplate(), |
|||
ItemTemplate = ItemTemplate(), |
|||
VirtualizationMode = ItemVirtualizationMode.Simple, |
|||
}; |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Return_IsLogicalScrollEnabled_True() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Parent_ScrollContentPresenter_Properties_Should_Be_Set() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var scroll = (ScrollContentPresenter)target.Parent; |
|||
Assert.Equal(new Size(0, 20), scroll.Extent); |
|||
Assert.Equal(new Size(0, 10), scroll.Viewport); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Fill_Panel_With_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
|
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Only_Create_Enough_Containers_To_Display_All_Items() |
|||
{ |
|||
var target = CreateTarget(itemCount: 2); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(2, target.Panel.Children.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Expand_To_Fit_Containers_When_Flexible_Size() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(Size.Infinity); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(new Size(10, 200), target.DesiredSize); |
|||
Assert.Equal(new Size(10, 200), target.Bounds.Size); |
|||
Assert.Equal(20, target.Panel.Children.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Initial_Item_DataContexts_Should_Be_Correct() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i], target.Panel.Children[i].DataContext); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Add_New_Items_When_Control_Is_Enlarged() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
|
|||
target.Measure(new Size(120, 120)); |
|||
target.Arrange(new Rect(0, 0, 100, 120)); |
|||
|
|||
Assert.Equal(12, target.Panel.Children.Count); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i], target.Panel.Children[i].DataContext); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control() |
|||
{ |
|||
var target = CreateTarget(mode: ItemVirtualizationMode.None); |
|||
var scroll = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroll.Measure(new Size(100, 100)); |
|||
scroll.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(20, target.Panel.Children.Count); |
|||
Assert.Equal(new Size(10, 200), scroll.Extent); |
|||
Assert.Equal(new Size(100, 100), scroll.Viewport); |
|||
|
|||
target.VirtualizationMode = ItemVirtualizationMode.Simple; |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
Assert.Equal(new Size(0, 20), scroll.Extent); |
|||
Assert.Equal(new Size(0, 10), scroll.Viewport); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.RealLayoutManager)) |
|||
{ |
|||
var target = CreateTarget(mode: ItemVirtualizationMode.None); |
|||
var scroll = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroll.Measure(new Size(100, 100)); |
|||
scroll.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
// Ensure than an intermediate measure pass doesn't add more controls than it
|
|||
// should. This can happen if target gets measured with Size.Infinity which
|
|||
// is what the available size should be when VirtualizationMode == None but not
|
|||
// what it should after VirtualizationMode is changed to Simple.
|
|||
target.Panel.Children.CollectionChanged += (s, e) => |
|||
{ |
|||
Assert.InRange(target.Panel.Children.Count, 0, 10); |
|||
}; |
|||
|
|||
target.VirtualizationMode = ItemVirtualizationMode.Simple; |
|||
LayoutManager.Instance.ExecuteLayoutPass(); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var scroll = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroll.Measure(new Size(100, 100)); |
|||
scroll.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
Assert.Equal(new Size(0, 20), scroll.Extent); |
|||
Assert.Equal(new Size(0, 10), scroll.Viewport); |
|||
|
|||
target.VirtualizationMode = ItemVirtualizationMode.None; |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
// Here - unlike changing the other way - we need to do a layout pass on the scroll
|
|||
// content presenter as non-logical scroll values are only updated on arrange.
|
|||
scroll.Measure(new Size(100, 100)); |
|||
scroll.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(20, target.Panel.Children.Count); |
|||
Assert.Equal(new Size(10, 200), scroll.Extent); |
|||
Assert.Equal(new Size(100, 100), scroll.Viewport); |
|||
} |
|||
|
|||
private static ItemsPresenter CreateTarget( |
|||
ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, |
|||
Orientation orientation = Orientation.Vertical, |
|||
bool useContainers = true, |
|||
int itemCount = 20) |
|||
{ |
|||
ItemsPresenter result; |
|||
var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); |
|||
|
|||
var scroller = new ScrollContentPresenter |
|||
{ |
|||
Content = result = new TestItemsPresenter(useContainers) |
|||
{ |
|||
Items = items, |
|||
ItemsPanel = VirtualizingPanelTemplate(orientation), |
|||
ItemTemplate = ItemTemplate(), |
|||
VirtualizationMode = mode, |
|||
} |
|||
}; |
|||
|
|||
scroller.UpdateChild(); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static IDataTemplate ItemTemplate() |
|||
{ |
|||
return new FuncDataTemplate<string>(x => new Canvas |
|||
{ |
|||
Width = 10, |
|||
Height = 10, |
|||
}); |
|||
} |
|||
|
|||
private static ITemplate<IPanel> VirtualizingPanelTemplate( |
|||
Orientation orientation = Orientation.Vertical) |
|||
{ |
|||
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel |
|||
{ |
|||
Orientation = orientation, |
|||
}); |
|||
} |
|||
|
|||
private class TestItemsPresenter : ItemsPresenter |
|||
{ |
|||
private bool _useContainers; |
|||
|
|||
public TestItemsPresenter(bool useContainers) |
|||
{ |
|||
_useContainers = useContainers; |
|||
} |
|||
|
|||
protected override IItemContainerGenerator CreateItemContainerGenerator() |
|||
{ |
|||
return _useContainers ? |
|||
new ItemContainerGenerator<TestContainer>(this, TestContainer.ContentProperty, null) : |
|||
new ItemContainerGenerator(this); |
|||
} |
|||
} |
|||
|
|||
private class TestContainer : ContentControl |
|||
{ |
|||
public TestContainer() |
|||
{ |
|||
Width = 10; |
|||
Height = 10; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,683 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.Generators; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Input; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests.Presenters |
|||
{ |
|||
public class ItemsPresenterTests_Virtualization_Simple |
|||
{ |
|||
[Fact] |
|||
public void Should_Return_Items_Count_For_Extent_Vertical() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Return_Items_Count_For_Extent_Horizontal() |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
|
|||
target.ApplyTemplate(); |
|||
|
|||
Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal() |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Remove_Items_When_Control_Is_Shrank() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
|
|||
target.Measure(new Size(100, 80)); |
|||
target.Arrange(new Rect(0, 0, 100, 80)); |
|||
|
|||
Assert.Equal(8, target.Panel.Children.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Add_New_Containers_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
Assert.Equal(10, target.Panel.Children.Count); |
|||
|
|||
((IScrollable)target).Offset = new Vector(0, 10); |
|||
target.Measure(new Size(120, 120)); |
|||
target.Arrange(new Rect(0, 0, 100, 120)); |
|||
|
|||
Assert.Equal(12, target.Panel.Children.Count); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i + 8], target.Panel.Children[i].DataContext); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Update_Containers_When_Items_Changes() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
target.Items = new[] { "foo", "bar", "baz" }; |
|||
|
|||
Assert.Equal(3, target.Panel.Children.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 95)); |
|||
target.Arrange(new Rect(0, 0, 100, 95)); |
|||
|
|||
Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 95)); |
|||
target.Arrange(new Rect(0, 0, 100, 95)); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 11); |
|||
|
|||
var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); |
|||
Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(10, minIndex); |
|||
Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 10); |
|||
|
|||
minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); |
|||
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(10, minIndex); |
|||
Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 11); |
|||
|
|||
minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); |
|||
Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(10, minIndex); |
|||
Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Inserting_Items_Should_Update_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 5); |
|||
|
|||
var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.Insert(6, "Inserted"); |
|||
expected.Insert(1, "Inserted"); |
|||
expected.RemoveAt(expected.Count - 1); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_First_Materialized_Item_Should_Update_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.RemoveAt(0); |
|||
expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList(); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 200)); |
|||
target.Arrange(new Rect(0, 0, 100, 200)); |
|||
|
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(items, actual); |
|||
|
|||
items.RemoveAt(2); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(items, actual); |
|||
|
|||
items.RemoveAt(items.Count - 2); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(items, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 200)); |
|||
target.Arrange(new Rect(0, 0, 100, 200)); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 5); |
|||
|
|||
var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.Remove(items.Last()); |
|||
expected.Remove(expected.Last()); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top() |
|||
{ |
|||
var target = CreateTarget(useAvaloniaList: true); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 10); |
|||
|
|||
var expected = Enumerable.Range(10, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (AvaloniaList<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.RemoveRange(18, 2); |
|||
expected = Enumerable.Range(8, 10).Select(x => $"Item {x}").ToList(); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top() |
|||
{ |
|||
var target = CreateTarget(useAvaloniaList: true); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
((ILogicalScrollable)target).Offset = new Vector(0, 9); |
|||
|
|||
var expected = Enumerable.Range(9, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (AvaloniaList<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.RemoveRange(15, 3); |
|||
expected = Enumerable.Range(7, 8).Select(x => $"Item {x}") |
|||
.Concat(Enumerable.Range(18, 2).Select(x => $"Item {x}")) |
|||
.ToList(); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Replacing_Items_Should_Update_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items[4] = expected[4] = "Replaced"; |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Moving_Items_Should_Update_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
items.Move(4, 8); |
|||
var i = expected[4]; |
|||
expected.RemoveAt(4); |
|||
expected.Insert(8, i); |
|||
|
|||
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Items_To_Null_Should_Remove_Containers() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); |
|||
var items = (ObservableCollection<string>)target.Items; |
|||
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
|
|||
target.Items = null; |
|||
|
|||
Assert.Empty(target.Panel.Children); |
|||
} |
|||
|
|||
public class Vertical |
|||
{ |
|||
[Fact] |
|||
public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var from = target.Panel.Children[5]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Down, |
|||
from); |
|||
|
|||
Assert.Same(target.Panel.Children[6], result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Down_Should_Scroll_If_Necessary() |
|||
{ |
|||
var target = CreateTarget(); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var from = target.Panel.Children[9]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Down, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[9], result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.RealLayoutManager)) |
|||
{ |
|||
var target = CreateTarget(); |
|||
var scroller = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroller.Measure(new Size(100, 95)); |
|||
scroller.Arrange(new Rect(0, 0, 100, 95)); |
|||
|
|||
var from = target.Panel.Children[8]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Down, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[8], result); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.RealLayoutManager)) |
|||
{ |
|||
var target = CreateTarget(); |
|||
var scroller = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroller.Measure(new Size(100, 95)); |
|||
scroller.Arrange(new Rect(0, 0, 100, 95)); |
|||
((ILogicalScrollable)target).Offset = new Vector(0, 11); |
|||
|
|||
var from = target.Panel.Children[1]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Up, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[0], result); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class Horizontal |
|||
{ |
|||
[Fact] |
|||
public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var from = target.Panel.Children[5]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Right, |
|||
from); |
|||
|
|||
Assert.Same(target.Panel.Children[6], result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Right_Should_Scroll_If_Necessary() |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var from = target.Panel.Children[9]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Right, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[9], result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.RealLayoutManager)) |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
var scroller = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroller.Measure(new Size(95, 100)); |
|||
scroller.Arrange(new Rect(0, 0, 95, 100)); |
|||
|
|||
var from = target.Panel.Children[8]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Right, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[8], result); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() |
|||
{ |
|||
var target = CreateTarget(orientation: Orientation.Horizontal); |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(95, 100)); |
|||
target.Arrange(new Rect(0, 0, 95, 100)); |
|||
((ILogicalScrollable)target).Offset = new Vector(11, 0); |
|||
|
|||
var from = target.Panel.Children[1]; |
|||
var result = ((ILogicalScrollable)target).GetControlInDirection( |
|||
NavigationDirection.Left, |
|||
from); |
|||
|
|||
Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); |
|||
Assert.Same(target.Panel.Children[0], result); |
|||
} |
|||
} |
|||
|
|||
public class WithContainers |
|||
{ |
|||
[Fact] |
|||
public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var containers = target.Panel.Children.ToList(); |
|||
var scroller = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroller.Offset = new Vector(0, 5); |
|||
|
|||
var scrolledContainers = containers |
|||
.Skip(5) |
|||
.Take(5) |
|||
.Concat(containers.Take(5)).ToList(); |
|||
|
|||
Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(scrolledContainers, target.Panel.Children); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); |
|||
} |
|||
|
|||
scroller.Offset = new Vector(0, 0); |
|||
Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(containers, target.Panel.Children); |
|||
|
|||
var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i], target.Panel.Children[i].DataContext); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Scrolling_More_Than_A_Page_Should_Recycle_Items() |
|||
{ |
|||
var target = CreateTarget(itemCount: 50); |
|||
var items = (IList<string>)target.Items; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 100, 100)); |
|||
|
|||
var containers = target.Panel.Children.ToList(); |
|||
var scroller = (ScrollContentPresenter)target.Parent; |
|||
|
|||
scroller.Offset = new Vector(0, 20); |
|||
|
|||
Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(containers, target.Panel.Children); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); |
|||
} |
|||
|
|||
scroller.Offset = new Vector(0, 0); |
|||
|
|||
Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); |
|||
Assert.Equal(containers, target.Panel.Children); |
|||
|
|||
for (var i = 0; i < target.Panel.Children.Count; ++i) |
|||
{ |
|||
Assert.Equal(items[i], target.Panel.Children[i].DataContext); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static ItemsPresenter CreateTarget( |
|||
Orientation orientation = Orientation.Vertical, |
|||
bool useContainers = true, |
|||
int itemCount = 20, |
|||
bool useAvaloniaList = false) |
|||
{ |
|||
ItemsPresenter result; |
|||
var itemsSource = Enumerable.Range(0, itemCount).Select(x => $"Item {x}"); |
|||
var items = useAvaloniaList ? |
|||
(IEnumerable)new AvaloniaList<string>(itemsSource) : |
|||
(IEnumerable)new ObservableCollection<string>(itemsSource); |
|||
|
|||
var scroller = new ScrollContentPresenter |
|||
{ |
|||
Content = result = new TestItemsPresenter(useContainers) |
|||
{ |
|||
Items = items, |
|||
ItemsPanel = VirtualizingPanelTemplate(orientation), |
|||
ItemTemplate = ItemTemplate(), |
|||
VirtualizationMode = ItemVirtualizationMode.Simple, |
|||
} |
|||
}; |
|||
|
|||
scroller.UpdateChild(); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static IDataTemplate ItemTemplate() |
|||
{ |
|||
return new FuncDataTemplate<string>(x => new Canvas |
|||
{ |
|||
Width = 10, |
|||
Height = 10, |
|||
}); |
|||
} |
|||
|
|||
private static ITemplate<IPanel> VirtualizingPanelTemplate( |
|||
Orientation orientation = Orientation.Vertical) |
|||
{ |
|||
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel |
|||
{ |
|||
Orientation = orientation, |
|||
}); |
|||
} |
|||
|
|||
private class TestItemsPresenter : ItemsPresenter |
|||
{ |
|||
private bool _useContainers; |
|||
|
|||
public TestItemsPresenter(bool useContainers) |
|||
{ |
|||
_useContainers = useContainers; |
|||
} |
|||
|
|||
protected override IItemContainerGenerator CreateItemContainerGenerator() |
|||
{ |
|||
return _useContainers ? |
|||
new ItemContainerGenerator<TestContainer>(this, TestContainer.ContentProperty, null) : |
|||
new ItemContainerGenerator(this); |
|||
} |
|||
} |
|||
|
|||
private class TestContainer : ContentControl |
|||
{ |
|||
public TestContainer() |
|||
{ |
|||
Width = 10; |
|||
Height = 10; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.LogicalTree; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class VirtualizingStackPanelTests |
|||
{ |
|||
public class Vertical |
|||
{ |
|||
[Fact] |
|||
public void Measure_Invokes_Controller_UpdateControls() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
var controller = new Mock<IVirtualizingController>(); |
|||
|
|||
target.Controller = controller.Object; |
|||
target.Measure(new Size(100, 100)); |
|||
|
|||
controller.Verify(x => x.UpdateControls()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Arrange_Invokes_Controller_UpdateControls() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
var controller = new Mock<IVirtualizingController>(); |
|||
|
|||
target.Controller = controller.Object; |
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(0, 0, 110, 110)); |
|||
|
|||
controller.Verify(x => x.UpdateControls()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
|
|||
Assert.Equal(new Size(0, 0), target.DesiredSize); |
|||
Assert.Equal(new Size(0, 0), target.Bounds.Size); |
|||
|
|||
Assert.False(target.IsFull); |
|||
Assert.Equal(0, target.OverflowCount); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
Assert.False(target.IsFull); |
|||
Assert.Equal(0, target.OverflowCount); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
Assert.True(target.IsFull); |
|||
Assert.Equal(0, target.OverflowCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reports_Overflow_After_Arrange() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(new Size(0, 0), target.Bounds.Size); |
|||
|
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
Assert.Equal(0, target.OverflowCount); |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(2, target.OverflowCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reports_Correct_Overflow_During_Arrange() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
var controller = new Mock<IVirtualizingController>(); |
|||
var called = false; |
|||
|
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 52 }); |
|||
target.Measure(new Size(100, 100)); |
|||
|
|||
controller.Setup(x => x.UpdateControls()).Callback(() => |
|||
{ |
|||
Assert.Equal(2, target.PixelOverflow); |
|||
Assert.Equal(0, target.OverflowCount); |
|||
called = true; |
|||
}); |
|||
|
|||
target.Controller = controller.Object; |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.True(called); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reports_PixelOverflow_After_Arrange() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
|
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 52 }); |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(2, target.PixelOverflow); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Reports_PixelOverflow_With_PixelOffset() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
|
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 52 }); |
|||
target.PixelOffset = 2; |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(2, target.PixelOverflow); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull() |
|||
{ |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
|
|||
target.Children.Add(new Canvas { Width = 50, Height = 50 }); |
|||
target.Children.Add(new Canvas { Width = 50, Height = 52 }); |
|||
target.PixelOffset = 55; |
|||
|
|||
target.Measure(new Size(100, 100)); |
|||
target.Arrange(new Rect(target.DesiredSize)); |
|||
|
|||
Assert.Equal(55, target.PixelOffset); |
|||
Assert.Equal(2, target.PixelOverflow); |
|||
Assert.True(target.IsFull); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() |
|||
{ |
|||
var presenter = new Mock<ILogical>().As<IControl>(); |
|||
var scrollable = presenter.As<ILogicalScrollable>(); |
|||
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); |
|||
var from = new Canvas(); |
|||
|
|||
scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); |
|||
|
|||
((ISetLogicalParent)target).SetParent(presenter.Object); |
|||
((INavigableContainer)target).GetControl(NavigationDirection.Next, from); |
|||
|
|||
scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue