Browse Source

Remove ItemsRepeater from the main repostiory (#14989)

See https://github.com/AvaloniaUI/Avalonia.Controls.ItemsRepeater
pull/14954/head
Max Katz 3 years ago
committed by GitHub
parent
commit
5537e97aa5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.Desktop.slnf
  2. 13
      Avalonia.sln
  3. 1
      samples/ControlCatalog/ControlCatalog.csproj
  4. 5
      samples/ControlCatalog/MainView.xaml
  5. 74
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  6. 157
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  7. 91
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  8. 18
      src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
  9. 27
      src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs
  10. 66
      src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs
  11. 67
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs
  12. 792
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  13. 24
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs
  14. 44
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs
  15. 35
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs
  16. 112
      src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs
  17. 117
      src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs
  18. 70
      src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs
  19. 54
      src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs
  20. 782
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs
  21. 550
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
  22. 119
      src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs
  23. 219
      src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs
  24. 463
      src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs
  25. 767
      src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs
  26. 44
      src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs
  27. 28
      src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs
  28. 45
      src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs
  29. 83
      src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs
  30. 31
      src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs
  31. 162
      src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs
  32. 96
      src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs
  33. 365
      src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs
  34. 61
      src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs
  35. 562
      src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs
  36. 204
      src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs
  37. 27
      src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs
  38. 37
      src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs
  39. 45
      src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs
  40. 42
      src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs
  41. 119
      src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs
  42. 195
      src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs
  43. 25
      src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs
  44. 336
      src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs
  45. 146
      src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs
  46. 4
      src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs
  47. 22
      tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj
  48. 24
      tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs
  49. 336
      tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs
  50. 1
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
  51. 36
      tests/Avalonia.LeakTests/ControlTests.cs
  52. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  53. 52
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  54. 1
      tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs

2
Avalonia.Desktop.slnf

@ -17,7 +17,6 @@
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
"src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj",
"src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj",
"src\\Avalonia.Controls\\Avalonia.Controls.csproj",
"src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj",
"src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",
@ -52,7 +51,6 @@
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Build.Tasks.UnitTest\\Avalonia.Build.Tasks.UnitTest.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
"tests\\Avalonia.Controls.ItemsRepeater.UnitTests\\Avalonia.Controls.ItemsRepeater.UnitTests.csproj",
"tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj",
"tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj",
"tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj",

13
Avalonia.sln

@ -252,10 +252,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Settings.StyleCop = Settings.StyleCop
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
@ -621,14 +617,6 @@ Global
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.Build.0 = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -779,7 +767,6 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}

1
samples/ControlCatalog/ControlCatalog.csproj

@ -27,7 +27,6 @@
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />

5
samples/ControlCatalog/MainView.xaml

@ -106,11 +106,6 @@
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:ImagePage />
</TabItem>
<TabItem Header="ItemsRepeater"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:ItemsRepeaterPage />
</TabItem>
<TabItem Header="Label">
<pages:LabelsPage />
</TabItem>

74
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -1,74 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:ControlCatalog.ViewModels"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage"
x:DataType="viewModels:ItemsRepeaterPageViewModel">
<UserControl.Styles>
<Style Selector="ItemsRepeater TextBlock.oddTemplate">
<Setter Property="Background" Value="Yellow" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock.evenTemplate">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</UserControl.Styles>
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd" x:DataType="viewModels:ItemsRepeaterPageViewModelItem">
<TextBlock Classes="oddTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate x:Key="even" x:DataType="viewModels:ItemsRepeaterPageViewModelItem">
<TextBlock Classes="evenTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<RecyclingElementFactory x:Key="elementFactory"
RecyclePool="{StaticResource RecyclePool}"
SelectTemplateKey="OnSelectTemplateKey">
<RecyclingElementFactory.Templates>
<StaticResource x:Key="odd" ResourceKey="odd" />
<StaticResource x:Key="even" ResourceKey="even" />
</RecyclingElementFactory.Templates>
</RecyclingElementFactory>
</UserControl.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
<TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="8 0" Spacing="4">
<ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
<ComboBoxItem>Stack - Vertical</ComboBoxItem>
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
</ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RemoveItem}">Remove Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
<Button Command="{Binding ResetItems}">Reset items</Button>
<Button x:Name="scrollToLast">Scroll to Last</Button>
<Button x:Name="scrollToRandom">Scroll to Random</Button>
<Button x:Name="scrollToSelected">Scroll to Selected</Button>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource CatalogBaseMediumColor}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Background="Transparent" ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource elementFactory}"/>
</ScrollViewer>
</Border>
</DockPanel>
</UserControl>

157
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -1,157 +0,0 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
private readonly ItemsRepeaterPageViewModel _viewModel;
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
private int _selectedIndex;
private Button _scrollToLast;
private Button _scrollToRandom;
private Button _scrollToSelected;
private Random _random = new Random(0);
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.Get<ItemsRepeater>("repeater");
_scroller = this.Get<ScrollViewer>("scroller");
_scrollToLast = this.Get<Button>("scrollToLast");
_scrollToRandom = this.Get<Button>("scrollToRandom");
_scrollToSelected = this.Get<Button>("scrollToSelected");
_repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown;
_scrollToLast.Click += scrollToLast_Click;
_scrollToRandom.Click += scrollToRandom_Click;
_scrollToSelected.Click += scrollToSelected_Click;
DataContext = _viewModel = new ItemsRepeaterPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e)
{
if (e.DataContext is ItemsRepeaterPageViewModelItem item)
{
e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd";
}
}
private void LayoutChanged(object sender, SelectionChangedEventArgs e)
{
if (_repeater == null)
{
return;
}
var comboBox = (ComboBox)sender;
switch (comboBox.SelectedIndex)
{
case 0:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
break;
case 1:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
break;
case 2:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Vertical,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
case 3:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Horizontal,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
case 4:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
_repeater.Layout = new WrapLayout
{
Orientation = Orientation.Vertical,
HorizontalSpacing = 20,
VerticalSpacing = 20
};
break;
case 5:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new WrapLayout
{
Orientation = Orientation.Horizontal,
HorizontalSpacing = 20,
VerticalSpacing = 20
};
break;
}
}
private void ScrollTo(int index)
{
System.Diagnostics.Debug.WriteLine("Scroll to " + index);
var element = _repeater.GetOrCreateElement(index);
((TopLevel)VisualRoot!).UpdateLayout();
element.BringIntoView();
}
private void RepeaterClick(object? sender, PointerPressedEventArgs e)
{
if ((e.Source as TextBlock)?.DataContext is ItemsRepeaterPageViewModelItem item)
{
_viewModel.SelectedItem = item;
_selectedIndex = _viewModel.Items.IndexOf(item);
}
}
private void RepeaterOnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.F5)
{
_viewModel.ResetItems();
}
}
private void scrollToLast_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_viewModel.Items.Count - 1);
}
private void scrollToRandom_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_random.Next(_viewModel.Items.Count - 1));
}
private void scrollToSelected_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_selectedIndex);
}
}
}

91
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -1,91 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Media;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ItemsRepeaterPageViewModel : ViewModelBase
{
private int _newItemIndex = 1;
private int _newGenerationIndex = 0;
private ObservableCollection<ItemsRepeaterPageViewModelItem> _items;
public ItemsRepeaterPageViewModel()
{
_items = CreateItems();
}
public ObservableCollection<ItemsRepeaterPageViewModelItem> Items
{
get => _items;
set => this.RaiseAndSetIfChanged(ref _items, value);
}
public ItemsRepeaterPageViewModelItem? SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new ItemsRepeaterPageViewModelItem(index + 1, $"New Item {_newItemIndex++}"));
}
public void RemoveItem()
{
if (SelectedItem is not null)
{
Items.Remove(SelectedItem);
SelectedItem = null;
}
else if (Items.Count > 0)
{
Items.RemoveAt(Items.Count - 1);
}
}
public void RandomizeHeights()
{
var random = new Random();
foreach (var i in Items)
{
i.Height = random.Next(240) + 10;
}
}
public void ResetItems()
{
Items = CreateItems();
}
private ObservableCollection<ItemsRepeaterPageViewModelItem> CreateItems()
{
var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]";
_newGenerationIndex++;
return new ObservableCollection<ItemsRepeaterPageViewModelItem>(
Enumerable.Range(1, 100000).Select(i => new ItemsRepeaterPageViewModelItem(i, $"Item {i.ToString()} {suffix}")));
}
}
public class ItemsRepeaterPageViewModelItem : ViewModelBase
{
private double _height = double.NaN;
public ItemsRepeaterPageViewModelItem(int index, string text)
{
Index = index;
Text = text;
}
public int Index { get; }
public string Text { get; }
public double Height
{
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
}
}

18
src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj

@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0</TargetFrameworks>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

27
src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs

@ -1,27 +0,0 @@
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
public abstract class ElementFactory : IElementFactory
{
public Control Build(object? data)
{
return GetElementCore(new ElementFactoryGetArgs { Data = data });
}
public Control GetElement(ElementFactoryGetArgs args)
{
return GetElementCore(args);
}
public bool Match(object? data) => true;
public void RecycleElement(ElementFactoryRecycleArgs args)
{
RecycleElementCore(args);
}
protected abstract Control GetElementCore(ElementFactoryGetArgs args);
protected abstract void RecycleElementCore(ElementFactoryRecycleArgs args);
}
}

66
src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs

@ -1,66 +0,0 @@
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
/// <summary>
/// Represents the optional arguments to use when calling an implementation of the
/// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
/// </summary>
public class ElementFactoryGetArgs
{
/// <summary>
/// Gets or sets the data item for which an appropriate element tree should be realized
/// when calling <see cref="IElementFactory.GetElement"/>.
/// </summary>
public object? Data { get; set; }
/// <summary>
/// Gets or sets the <see cref="Control"/> that is expected to be the parent of the
/// realized element from <see cref="IElementFactory.GetElement"/>.
/// </summary>
public Control? Parent { get; set; }
/// <summary>
/// Gets or sets the index of the item that should be realized.
/// </summary>
public int Index { get; set; }
}
/// <summary>
/// Represents the optional arguments to use when calling an implementation of the
/// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
/// </summary>
public class ElementFactoryRecycleArgs
{
/// <summary>
/// Gets or sets the <see cref="Control"/> to recycle when calling
/// <see cref="IElementFactory.RecycleElement"/>.
/// </summary>
public Control? Element { get; set; }
/// <summary>
/// Gets or sets the <see cref="Control"/> that is expected to be the parent of the
/// realized element from <see cref="IElementFactory.GetElement"/>.
/// </summary>
public Control? Parent { get; set; }
}
/// <summary>
/// A data template that supports creating and recycling elements for an <see cref="ItemsRepeater"/>.
/// </summary>
public interface IElementFactory : IDataTemplate
{
/// <summary>
/// Gets an <see cref="Control"/>.
/// </summary>
/// <param name="args">The element args.</param>
public Control GetElement(ElementFactoryGetArgs args);
/// <summary>
/// Recycles an <see cref="Control"/> that was previously retrieved using
/// <see cref="GetElement"/>.
/// </summary>
/// <param name="args">The recycle args.</param>
public void RecycleElement(ElementFactoryRecycleArgs args);
}
}

67
src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs

@ -1,67 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
internal class ItemTemplateWrapper : IElementFactory
{
private readonly IDataTemplate _dataTemplate;
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
public Control Build(object? param) => GetElement(null, param);
public bool Match(object? data) => _dataTemplate.Match(data);
public Control GetElement(ElementFactoryGetArgs args)
{
return GetElement(args.Parent, args.Data);
}
public void RecycleElement(ElementFactoryRecycleArgs args)
{
RecycleElement(args.Parent, args.Element!);
}
private Control GetElement(Control? parent, object? data)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
Control? element = null;
if (recyclePool != null)
{
// try to get an element from the recycle pool.
element = recyclePool.TryGetElement(string.Empty, parent);
}
if (element == null)
{
// no element was found in recycle pool, create a new element
element = selectedTemplate.Build(data)!;
// Associate template with element
element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
}
return element;
}
private void RecycleElement(Control? parent, Control element)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
if (recyclePool == null)
{
// No Recycle pool in the template, create one.
recyclePool = new RecyclePool();
RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
}
recyclePool.PutElement(element, "" /* key */, parent);
}
}
}

792
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@ -1,792 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(HorizontalCacheLength), 2.0);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsRepeater>();
/// <summary>
/// Defines the <see cref="ItemsSource"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsSourceProperty =
AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
nameof(ItemsSource),
o => o.ItemsSource,
(o, v) => o.ItemsSource = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.
/// </summary>
public static readonly StyledProperty<AttachedLayout?> LayoutProperty =
AvaloniaProperty.Register<ItemsRepeater, AttachedLayout?>(nameof(Layout), new StackLayout());
/// <summary>
/// Defines the <see cref="VerticalCacheLength"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0);
private static readonly StyledProperty<VirtualizationInfo?> VirtualizationInfoProperty =
AvaloniaProperty.RegisterAttached<ItemsRepeater, Control, VirtualizationInfo?>("VirtualizationInfo");
internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
private readonly ViewManager _viewManager;
private readonly ViewportManager _viewportManager;
private readonly TargetWeakEventSubscriber<ItemsRepeater, EventArgs> _layoutWeakSubscriber;
private IEnumerable? _itemsSource;
private RepeaterLayoutContext? _layoutContext;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
private bool _isLayoutInProgress;
private NotifyCollectionChangedEventArgs? _processingItemsSourceChange;
private ItemsRepeaterElementPreparedEventArgs? _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs? _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs? _elementIndexChangedArgs;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsRepeater"/> class.
/// </summary>
public ItemsRepeater()
{
_layoutWeakSubscriber = new TargetWeakEventSubscriber<ItemsRepeater, EventArgs>(
this, static (target, _, ev, _) =>
{
if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
target.InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
target.InvalidateMeasure();
});
_viewManager = new ViewManager(this);
_viewportManager = new ViewportManager(this);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
OnLayoutChanged(null, Layout);
}
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
RequestBringIntoViewEvent.AddClassHandler<ItemsRepeater>((x, e) => x.OnRequestBringIntoView(e));
}
/// <summary>
/// Gets or sets the layout used to size and position elements in the ItemsRepeater.
/// </summary>
/// <value>
/// The layout used to size and position elements. The default is a StackLayout with
/// vertical orientation.
/// </value>
public AttachedLayout? Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
/// <summary>
/// Gets or sets an object source used to generate the content of the ItemsRepeater.
/// </summary>
public IEnumerable? ItemsSource
{
get => _itemsSource;
set => SetAndRaise(ItemsSourceProperty, ref _itemsSource, value);
}
/// <summary>
/// Gets or sets the template used to display each item.
/// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling horizontally.
/// </summary>
public double HorizontalCacheLength
{
get => GetValue(HorizontalCacheLengthProperty);
set => SetValue(HorizontalCacheLengthProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling vertically.
/// </summary>
public double VerticalCacheLength
{
get => GetValue(VerticalCacheLengthProperty);
set => SetValue(VerticalCacheLengthProperty, value);
}
/// <summary>
/// Gets a standardized view of the supported interactions between a given Items object and
/// the ItemsRepeater control and its associated components.
/// </summary>
public ItemsSourceView? ItemsSourceView { get; private set; }
internal IElementFactory? ItemTemplateShim { get; set; }
internal Point LayoutOrigin { get; set; }
internal object? LayoutState { get; set; }
internal Control? MadeAnchor => _viewportManager.MadeAnchor;
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
internal Control? SuggestedAnchor => _viewportManager.SuggestedAnchor;
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
private RepeaterLayoutContext LayoutContext => _layoutContext ??= new RepeaterLayoutContext(this);
event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is Control control
? GetElementIndex(control)
: -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
count = ItemsSourceView?.Count ?? 0;
return true;
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
/// <remarks>
/// This event is raised immediately each time an element is cleared, such as when it falls
/// outside the range of realized items. Elements are cleared when they become available
/// for re-use.
/// </remarks>
public event EventHandler<ItemsRepeaterElementClearingEventArgs>? ElementClearing;
/// <summary>
/// Occurs for each realized <see cref="Control"/> when the index for the item it
/// represents has changed.
/// </summary>
/// <remarks>
/// When you use ItemsRepeater to build a more complex control that supports specific
/// interactions on the child elements (such as selection or click), it is useful to be
/// able to keep an up-to-date identifier for the backing data item.
///
/// This event is raised for each realized Control where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
/// </remarks>
public event EventHandler<ItemsRepeaterElementIndexChangedEventArgs>? ElementIndexChanged;
/// <summary>
/// Occurs each time an element is prepared for use.
/// </summary>
/// <remarks>
/// The prepared element might be newly created or an existing element that is being re-
/// used.
/// </remarks>
public event EventHandler<ItemsRepeaterElementPreparedEventArgs>? ElementPrepared;
/// <summary>
/// Retrieves the index of the item from the data source that corresponds to the specified
/// <see cref="Control"/>.
/// </summary>
/// <param name="element">
/// The element that corresponds to the item to get the index of.
/// </param>
/// <returns>
/// The index of the item from the data source that corresponds to the specified UIElement,
/// or -1 if the element is not supported.
/// </returns>
public int GetElementIndex(Control element) => GetElementIndexImpl(element);
/// <summary>
/// Retrieves the realized UIElement that corresponds to the item at the specified index in
/// the data source.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <returns>
/// he UIElement that corresponds to the item at the specified index if the item is
/// realized, or null if the item is not realized.
/// </returns>
public Control? TryGetElement(int index) => GetElementFromIndexImpl(index);
/// <summary>
/// Retrieves the UIElement that corresponds to the item at the specified index in the
/// data source.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <returns>
/// An <see cref="Control"/> that corresponds to the item at the specified index. If the
/// item is not realized, a new UIElement is created.
/// </returns>
public Control GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
internal void PinElement(Control element) => _viewManager.UpdatePin(element, true);
internal void UnpinElement(Control element) => _viewManager.UpdatePin(element, false);
internal static VirtualizationInfo? TryGetVirtualizationInfo(Control? element)
{
return element?.GetValue(VirtualizationInfoProperty);
}
internal static VirtualizationInfo GetVirtualizationInfo(Control element)
{
var result = element.GetValue(VirtualizationInfoProperty);
if (result == null)
{
result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
}
return result;
}
private protected override void InvalidateMeasureOnChildrenChanged()
{
// Don't invalidate measure when children change.
}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_viewportManager.OnOwnerMeasuring();
_isLayoutInProgress = true;
try
{
_viewManager.PrunePinnedElements();
var extent = new Rect();
var desiredSize = new Size();
var layout = Layout;
if (layout != null)
{
var layoutContext = LayoutContext;
desiredSize = layout.Measure(layoutContext, availableSize);
extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
// Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
// call GetElementAt(index).
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
if (virtInfo.Owner == ElementOwner.Layout &&
virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "AutoClear - {Index}", virtInfo.Index);
ClearElementImpl(element);
}
}
}
_viewportManager.SetLayoutExtent(extent);
return desiredSize;
}
finally
{
_isLayoutInProgress = false;
}
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size finalSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_isLayoutInProgress = true;
try
{
var arrangeSize = Layout?.Arrange(LayoutContext, finalSize) ?? default;
// The view manager might clear elements during this call.
// That's why we call it before arranging cleared elements
// off screen.
_viewManager.OnOwnerArranged();
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
virtInfo.KeepAlive = false;
if (virtInfo.Owner == ElementOwner.ElementFactory ||
virtInfo.Owner == ElementOwner.PinnedPool)
{
// Toss it away. And arrange it with size 0 so that XYFocus won't use it.
element.Arrange(new Rect(
ClearedElementsArrangePosition.X - element.DesiredSize.Width,
ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
0,
0));
}
else
{
var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds;
_viewportManager.RegisterScrollAnchorCandidate(element, virtInfo);
}
}
_viewportManager.OnOwnerArranged();
return arrangeSize;
}
finally
{
_isLayoutInProgress = false;
}
}
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewportManager.ResetScrollers();
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == ItemsSourceProperty)
{
var (oldEnumerable, newEnumerable) = change.GetOldAndNewValue<IEnumerable?>();
if (oldEnumerable != newEnumerable)
{
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = ItemsSourceView.GetOrCreate(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
}
else if (change.Property == ItemTemplateProperty)
{
var (oldvalue, newValue) = change.GetOldAndNewValue<IDataTemplate?>();
OnItemTemplateChanged(oldvalue, newValue);
}
else if (change.Property == LayoutProperty)
{
var (oldvalue, newValue) = change.GetOldAndNewValue<AttachedLayout>();
OnLayoutChanged(oldvalue, newValue);
}
else if (change.Property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = change.GetNewValue<double>();
}
else if (change.Property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = change.GetNewValue<double>();
}
base.OnPropertyChanged(change);
}
internal Control GetElementImpl(int index, bool forceCreate, bool suppressAutoRecycle)
{
var element = _viewManager.GetElement(index, forceCreate, suppressAutoRecycle);
return element;
}
internal void ClearElementImpl(Control element)
{
// Clearing an element due to a collection change
// is more strict in that pinned elements will be forcibly
// unpinned and sent back to the view generator.
var isClearedDueToCollectionChange =
_processingItemsSourceChange != null &&
(_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
_viewManager.ClearElement(element, isClearedDueToCollectionChange);
_viewportManager.OnElementCleared(element, GetVirtualizationInfo(element));
}
private int GetElementIndexImpl(Control element)
{
// Verify that element is actually a child of this ItemsRepeater
var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
}
private Control? GetElementFromIndexImpl(int index)
{
Control? result = null;
var children = Children;
for (var i = 0; i < children.Count && result == null; ++i)
{
var element = children[i];
var virtInfo = TryGetVirtualizationInfo(element);
if (virtInfo?.IsRealized == true && virtInfo.Index == index)
{
result = element;
}
}
return result;
}
private Control GetOrCreateElementImpl(int index)
{
if (index >= 0 && index >= (ItemsSourceView?.Count ?? 0))
{
throw new ArgumentException("Argument index is invalid.", nameof(index));
}
if (_isLayoutInProgress)
{
throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
}
var element = GetElementFromIndexImpl(index);
bool isAnchorOutsideRealizedRange = element == null;
if (isAnchorOutsideRealizedRange)
{
if (Layout == null)
{
throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
}
element = (Control)LayoutContext.GetOrCreateElementAt(index);
element.Measure(Size.Infinity);
}
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
InvalidateMeasure();
return element!;
}
internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
var index = virtInfo.Index;
_viewportManager.OnElementPrepared(element, virtInfo);
if (ElementPrepared != null)
{
if (_elementPreparedArgs == null)
{
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
}
else
{
_elementPreparedArgs.Update(element, index);
}
ElementPrepared(this, _elementPreparedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index));
}
internal void OnElementClearing(Control element)
{
if (ElementClearing != null)
{
if (_elementClearingArgs == null)
{
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
}
else
{
_elementClearingArgs.Update(element);
}
ElementClearing(this, _elementClearingArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1));
}
internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
{
if (ElementIndexChanged != null)
{
if (_elementIndexChangedArgs == null)
{
_elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
}
else
{
_elementIndexChangedArgs.Update(element, oldIndex, newIndex);
}
ElementIndexChanged(this, _elementIndexChangedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex));
}
private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
ItemsSourceView = newValue;
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;
}
if (Layout != null)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
try
{
_processingItemsSourceChange = args;
if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChanged(LayoutContext, newValue, args);
}
else if (Layout is NonVirtualizingLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
Children.Clear();
}
}
finally
{
_processingItemsSourceChange = null;
}
InvalidateMeasure();
}
}
private void OnItemTemplateChanged(IDataTemplate? oldValue, IDataTemplate? newValue)
{
if (_isLayoutInProgress && oldValue != null)
{
throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
}
// Since the ItemTemplate has changed, we need to re-evaluate all the items that
// have already been created and are now in the tree. The easiest way to do that
// would be to do a reset.. Note that this has to be done before we change the template
// so that the cleared elements go back into the old template.
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
_processingItemsSourceChange = args;
try
{
virtualLayout.OnItemsChanged(LayoutContext, newValue, args);
}
finally
{
_processingItemsSourceChange = null;
}
}
else if (Layout is NonVirtualizingLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
}
ItemTemplateShim = newValue switch
{
IElementFactory factory => factory,
null => null,
_ => new ItemTemplateWrapper(newValue)
};
InvalidateMeasure();
}
private void OnLayoutChanged(AttachedLayout? oldValue, AttachedLayout? newValue)
{
if (_isLayoutInProgress)
{
throw new InvalidOperationException("Layout cannot be changed during layout.");
}
_viewManager.OnLayoutChanging();
if (oldValue != null)
{
oldValue.UninitializeForContext(LayoutContext);
AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber);
AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber);
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
LayoutState = null;
}
if (newValue != null)
{
newValue.InitializeForContext(LayoutContext);
AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber);
AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber);
}
bool isVirtualizingLayout = newValue is VirtualizingLayout;
_viewportManager.OnLayoutChanged(isVirtualizingLayout);
InvalidateMeasure();
}
private void OnItemsSourceViewChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
if (_isLayoutInProgress)
{
// Bad things will follow if the data changes while we are in the middle of a layout pass.
throw new InvalidOperationException("Changes in data source are not allowed during layout.");
}
if (IsProcessingCollectionChange)
{
throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
}
_processingItemsSourceChange = args;
try
{
_viewManager.OnItemsSourceChanged(sender, args);
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChanged(LayoutContext, sender, args);
}
else
{
// NonVirtualizingLayout
InvalidateMeasure();
}
}
}
finally
{
_processingItemsSourceChange = null;
}
}
private void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
_viewportManager.OnBringIntoViewRequested(e);
}
}
}

24
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs

@ -1,24 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementClearing"/> event.
/// </summary>
public class ItemsRepeaterElementClearingEventArgs : EventArgs
{
internal ItemsRepeaterElementClearingEventArgs(Control element) => Element = element;
/// <summary>
/// Gets the element that is being cleared for re-use.
/// </summary>
public Control Element { get; private set; }
internal void Update(Control element) => Element = element;
}
}

44
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs

@ -1,44 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementIndexChanged"/> event.
/// </summary>
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{
internal ItemsRepeaterElementIndexChangedEventArgs(Control element, int oldIndex, int newIndex)
{
Element = element;
OldIndex = oldIndex;
NewIndex = newIndex;
}
/// <summary>
/// Get the element for which the index changed.
/// </summary>
public Control Element { get; private set; }
/// <summary>
/// Gets the index of the element after the change.
/// </summary>
public int NewIndex { get; private set; }
/// <summary>
/// Gets the index of the element before the change.
/// </summary>
public int OldIndex { get; private set; }
internal void Update(Control element, int oldIndex, int newIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
}

35
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs

@ -1,35 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementPrepared"/> event.
/// </summary>
public class ItemsRepeaterElementPreparedEventArgs
{
internal ItemsRepeaterElementPreparedEventArgs(Control element, int index)
{
Element = element;
Index = index;
}
/// <summary>
/// Gets the prepared element.
/// </summary>
public Control Element { get; private set; }
/// <summary>
/// Gets the index of the item the element was prepared for.
/// </summary>
public int Index { get; private set; }
internal void Update(Control element, int index)
{
Element = element;
Index = index;
}
}
}

112
src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs

@ -1,112 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
public class RecyclePool
{
internal static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<RecyclePool, Control, IDataTemplate>("OriginTemplate");
internal static readonly AttachedProperty<string> ReuseKeyProperty =
AvaloniaProperty.RegisterAttached<RecyclePool, Control, string>("ReuseKey", string.Empty);
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
public static RecyclePool? GetPoolInstance(IDataTemplate dataTemplate)
{
s_pools.TryGetValue(dataTemplate, out var result);
return result;
}
public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
public void PutElement(Control element, string key, Control? owner)
{
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
var elementInfo = new ElementInfo(element, ownerAsPanel);
if (!_elements.TryGetValue(key, out var pool))
{
pool = new List<ElementInfo>();
_elements.Add(key, pool);
}
pool.Add(elementInfo);
}
public Control? TryGetElement(string key, Control? owner)
{
if (_elements.TryGetValue(key, out var elements))
{
if (elements.Count > 0)
{
// Prefer an element from the same owner or with no owner so that we don't incur
// the enter/leave cost during recycling.
// TODO: prioritize elements with the same owner to those without an owner.
var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
elements.Remove(elementInfo!);
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
if (elementInfo!.Owner != null && elementInfo.Owner != ownerAsPanel)
{
// Element is still under its parent. remove it from its parent.
var panel = elementInfo.Owner;
if (panel != null)
{
int childIndex = panel.Children.IndexOf(elementInfo.Element);
if (childIndex == -1)
{
throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
}
panel.Children.RemoveAt(childIndex);
}
}
return elementInfo.Element;
}
}
return null;
}
internal string GetReuseKey(Control element) => element.GetValue(ReuseKeyProperty);
internal void SetReuseKey(Control element, string value) => element.SetValue(ReuseKeyProperty, value);
private Panel? EnsureOwnerIsPanelOrNull(Control? owner)
{
if (owner is Panel panel)
{
return panel;
}
else if (owner != null)
{
throw new InvalidOperationException("Owner must be IPanel or null.");
}
return null;
}
private class ElementInfo
{
public ElementInfo(Control element, Panel? owner)
{
Element = element;
Owner = owner;
}
public Control Element { get; }
public Panel? Owner { get;}
}
}
}

117
src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs

@ -1,117 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
public class SelectTemplateEventArgs : EventArgs
{
public string? TemplateKey { get; set; }
public object? DataContext { get; internal set; }
public Control? Owner { get; internal set; }
}
public class RecyclingElementFactory : ElementFactory
{
private RecyclePool? _recyclePool;
private IDictionary<string, IDataTemplate>? _templates;
private SelectTemplateEventArgs? _args;
public RecyclingElementFactory()
{
Templates = new Dictionary<string, IDataTemplate>();
}
public RecyclePool RecyclePool
{
get => _recyclePool ??= new RecyclePool();
set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value));
}
public IDictionary<string, IDataTemplate> Templates
{
get => _templates ??= new Dictionary<string, IDataTemplate>();
set => _templates = value ?? throw new ArgumentNullException(nameof(value));
}
public event EventHandler<SelectTemplateEventArgs>? SelectTemplateKey;
protected override Control GetElementCore(ElementFactoryGetArgs args)
{
if (_templates == null || _templates.Count == 0)
{
throw new InvalidOperationException("Templates cannot be empty.");
}
var templateKey = Templates.Count == 1 ?
Templates.First().Key :
OnSelectTemplateKeyCore(args.Data, args.Parent);
if (string.IsNullOrEmpty(templateKey))
{
// Note: We could allow null/whitespace, which would work as long as
// the recycle pool is not shared. in order to make this work in all cases
// currently we validate that a valid template key is provided.
throw new InvalidOperationException("Template key cannot be null or empty.");
}
// Get an element from the Recycle Pool or create one
var element = RecyclePool.TryGetElement(templateKey, args.Parent);
if (element is null)
{
// No need to call HasKey if there is only one template.
if (Templates.Count > 1 && !Templates.ContainsKey(templateKey))
{
var message = $"No templates of key '{templateKey}' were found in the templates collection.";
throw new InvalidOperationException(message);
}
var dataTemplate = Templates[templateKey];
element = dataTemplate.Build(args.Data)!;
// Associate ReuseKey with element
RecyclePool.SetReuseKey(element, templateKey);
}
return element;
}
protected override void RecycleElementCore(ElementFactoryRecycleArgs args)
{
var element = args.Element!;
var key = RecyclePool.GetReuseKey(element);
RecyclePool.PutElement(element, key, args.Parent);
}
protected virtual string OnSelectTemplateKeyCore(object? dataContext, Control? owner)
{
if (SelectTemplateKey is not null)
{
_args ??= new SelectTemplateEventArgs();
_args.TemplateKey = null;
_args.DataContext = dataContext;
_args.Owner = owner;
try
{
SelectTemplateKey(this, _args);
}
finally
{
_args.DataContext = null;
_args.Owner = null;
}
}
if (string.IsNullOrEmpty(_args?.TemplateKey))
{
throw new InvalidOperationException(
"Please provide a valid template identifier in the handler for the SelectTemplateKey event.");
}
return _args!.TemplateKey!;
}
}
}

70
src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs

@ -1,70 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Layout;
using Avalonia.Logging;
namespace Avalonia.Controls
{
internal class RepeaterLayoutContext : VirtualizingLayoutContext
{
private readonly ItemsRepeater _owner;
public RepeaterLayoutContext(ItemsRepeater owner)
{
_owner = owner;
}
protected override Point LayoutOriginCore
{
get => _owner.LayoutOrigin;
set => _owner.LayoutOrigin = value;
}
protected override object? LayoutStateCore
{
get => _owner.LayoutState;
set => _owner.LayoutState = value;
}
protected override int RecommendedAnchorIndexCore
{
get
{
int anchorIndex = -1;
var anchor = _owner.SuggestedAnchor;
if (anchor != null)
{
anchorIndex = _owner.GetElementIndex(anchor);
}
return anchorIndex;
}
}
protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
protected override Layoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
{
return _owner.GetElementImpl(
index,
options.HasFlag(ElementRealizationOptions.ForceCreate),
options.HasFlag(ElementRealizationOptions.SuppressAutoRecycle));
}
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView!.GetAt(index)!;
protected override void RecycleElementCore(Layoutable element)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "RepeaterLayout - RecycleElement: {Index}", _owner.GetElementIndex((Control)element));
_owner.ClearElementImpl((Control)element);
}
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}
}

54
src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs

@ -1,54 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls
{
internal class UniqueIdElementPool : IEnumerable<KeyValuePair<string, Control>>
{
private readonly Dictionary<string, Control> _elementMap = new Dictionary<string, Control>();
private readonly ItemsRepeater _owner;
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
public void Add(Control element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var key = virtInfo.UniqueId!;
if (_elementMap.ContainsKey(key))
{
throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
}
_elementMap.Add(key, element);
}
public Control? Remove(int index)
{
// Check if there is already a element in the mapping and if so, use it.
string key = _owner.ItemsSourceView!.KeyFromIndex(index);
if (_elementMap.TryGetValue(key, out var element))
{
_elementMap.Remove(key);
}
return element;
}
public void Clear()
{
_elementMap.Clear();
}
public IEnumerator<KeyValuePair<string, Control>> GetEnumerator() => _elementMap.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

782
src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs

@ -1,782 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal sealed class ViewManager
{
private const int FirstRealizedElementIndexDefault = int.MaxValue;
private const int LastRealizedElementIndexDefault = int.MinValue;
private readonly ItemsRepeater _owner;
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>();
private readonly UniqueIdElementPool _resetPool;
private Control? _lastFocusedElement;
private bool _isDataSourceStableResetPending;
private ElementFactoryGetArgs? _elementFactoryGetArgs;
private ElementFactoryRecycleArgs? _elementFactoryRecycleArgs;
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
private bool _eventsSubscribed;
public ViewManager(ItemsRepeater owner)
{
_owner = owner;
_resetPool = new UniqueIdElementPool(owner);
}
public Control GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
{
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
if (element == null)
{
// check if this is the anchor made through repeater in preparation
// for a bring into view.
var madeAnchor = _owner.MadeAnchor;
if (madeAnchor != null)
{
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
if (anchorVirtInfo!.Index == index)
{
element = madeAnchor;
}
}
}
if (element == null) { element = GetElementFromUniqueIdResetPool(index); }
if (element == null) { element = GetElementFromPinnedElements(index); }
if (element == null) { element = GetElementFromElementFactory(index); }
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (suppressAutoRecycle)
{
virtInfo!.AutoRecycleCandidate = false;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index);
}
else
{
virtInfo!.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index);
}
return element;
}
public void ClearElement(Control element, bool isClearedDueToCollectionChange)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var index = virtInfo.Index;
bool cleared =
ClearElementToUniqueIdResetPool(element, virtInfo) ||
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
if (!cleared)
{
ClearElementToElementFactory(element);
}
// Both First and Last indices need to be valid or default.
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
{
// First and last were pointing to the same element and that is going away.
InvalidateRealizedIndicesHeldByLayout();
}
else if (index == _firstRealizedElementIndexHeldByLayout)
{
// The FirstElement is going away, shrink the range by one.
++_firstRealizedElementIndexHeldByLayout;
}
else if (index == _lastRealizedElementIndexHeldByLayout)
{
// Last element is going away, shrink the range by one at the end.
--_lastRealizedElementIndexHeldByLayout;
}
else
{
// Index is either outside the range we are keeping track of or inside the range.
// In both these cases, we just keep the range we have. If this clear was due to
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
}
}
// We need to clear the datacontext to prevent crashes from happening,
// however we only do that if we were the ones setting it.
// That is when one of the following is the case (numbering taken from line ~642):
// 1.2 No ItemTemplate, data is not a UIElement
// 2.1 ItemTemplate, data is not FrameworkElement
// 2.2.2 Itemtemplate, data is FrameworkElement, ElementFactory returned Element different to data
//
// In all of those three cases, we the ItemTemplateShim is NOT null.
// Luckily when we create the items, we store whether we were the once setting the DataContext.
public void ClearElementToElementFactory(Control element)
{
_owner.OnElementClearing(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
// During creation of this object, we were the one setting the DataContext, so clear it now.
if (virtInfo.MustClearDataContext)
{
element.DataContext = null;
}
if (_owner.ItemTemplateShim != null)
{
var context = _elementFactoryRecycleArgs ??= new ElementFactoryRecycleArgs();
context.Element = element;
context.Parent = _owner;
_owner.ItemTemplateShim.RecycleElement(context);
context.Element = null;
context.Parent = null;
}
else
{
// No ItemTemplate to recycle to, remove the element from the children collection.
if (!_owner.Children.Remove(element))
{
throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection.");
}
}
if (_lastFocusedElement == element)
{
// Focused element is going away. Remove the tracked last focused element
// and pick a reasonable next focus if we can find one within the layout
// realized elements.
MoveFocusFromClearedIndex(virtInfo.Index);
}
}
private void MoveFocusFromClearedIndex(int clearedIndex)
{
var focusCandidate = FindFocusCandidate(clearedIndex, out var focusedChild);
if (focusCandidate != null)
{
focusCandidate.Focus();
_lastFocusedElement = focusedChild;
// Add pin to hold the focused element.
UpdatePin(focusedChild!, true /* addPin */);
}
else
{
// We could not find a candidate.
_lastFocusedElement = null;
}
}
Control? FindFocusCandidate(int clearedIndex, out Control? focusedChild)
{
// Walk through all the children and find elements with index before and after the cleared index.
// Note that during a delete the next element would now have the same index.
int previousIndex = int.MinValue;
int nextIndex = int.MaxValue;
Control? nextElement = null;
Control? previousElement = null;
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
int currentIndex = virtInfo.Index;
if (currentIndex < clearedIndex)
{
if (currentIndex > previousIndex)
{
previousIndex = currentIndex;
previousElement = child;
}
}
else if (currentIndex >= clearedIndex)
{
// Note that we use >= above because if we deleted the focused element,
// the next element would have the same index now.
if (currentIndex < nextIndex)
{
nextIndex = currentIndex;
nextElement = child;
}
}
}
}
// TODO: Find the next element if one exists, if not use the previous element.
// If the container itself is not focusable, find a descendent that is.
focusedChild = nextElement;
return nextElement;
}
public int GetElementIndex(VirtualizationInfo? virtInfo)
{
if (virtInfo == null)
{
//Element is not a child of this ItemsRepeater.
return -1;
}
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
}
public void PrunePinnedElements()
{
EnsureEventSubscriptions();
// Go through pinned elements and make sure they still have
// a reason to be pinned.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (!virtInfo.IsPinned)
{
_pinnedPool.RemoveAt(i);
--i;
// Pinning was the only thing keeping this element alive.
ClearElementToElementFactory(elementInfo.PinnedElement);
}
}
}
public void UpdatePin(Control element, bool addPin)
{
var parent = element.GetVisualParent();
var child = (Visual)element;
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo((Control)child);
if (virtInfo.IsRealized)
{
if (addPin)
{
virtInfo.AddPin();
}
else if (virtInfo.IsPinned)
{
if (virtInfo.RemovePin() == 0)
{
// ElementFactory is invoked during the measure pass.
// We will clear the element then.
repeater.InvalidateMeasure();
}
}
}
}
child = parent;
parent = child.GetVisualParent();
}
}
public void OnItemsSourceChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
// Note: For items that have been removed, the index will not be touched. It will hold
// the old index before it was removed. It is not valid anymore.
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
var newIndex = args.NewStartingIndex;
var newCount = args.NewItems!.Count;
EnsureFirstLastRealizedIndices();
if (newIndex <= _lastRealizedElementIndexHeldByLayout)
{
_lastRealizedElementIndexHeldByLayout += newCount;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
else
{
// Indices held by layout are not affected
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
var element = elementInfo.PinnedElement;
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
// Two cases here
// case 1: oldCount == newCount
// indices are not affected. nothing to do here.
// case 2: oldCount != newCount
// Replaced with less or more items. This is like an insert or remove
// depending on the counts.
var oldStartIndex = args.OldStartingIndex;
var newStartingIndex = args.NewStartingIndex;
var oldCount = args.OldItems!.Count;
var newCount = args.NewItems!.Count;
if (oldStartIndex != newStartingIndex)
{
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
}
if (oldCount == 0)
{
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
}
if (newCount == 0)
{
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
}
int countChange = newCount - oldCount;
if (countChange != 0)
{
// countChange > 0 : countChange items were added
// countChange < 0 : -countChange items were removed
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (dataIndex >= oldStartIndex + oldCount)
{
UpdateElementIndex(element, virtInfo, dataIndex + countChange);
}
}
}
EnsureFirstLastRealizedIndices();
_lastRealizedElementIndexHeldByLayout += countChange;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
var oldStartIndex = args.OldStartingIndex;
var oldCount = args.OldItems!.Count;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
{
// If we are doing the mapping, remove the element who's data was removed.
_owner.ClearElementImpl(element);
}
else if (dataIndex >= (oldStartIndex + oldCount))
{
UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
}
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
case NotifyCollectionChangedAction.Reset:
// If we get multiple resets back to back before
// running layout, we dont have to clear all the elements again.
if (!_isDataSourceStableResetPending)
{
if (_owner.ItemsSourceView!.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
}
private void EnsureFirstLastRealizedIndices()
{
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
{
// This will ensure that the indexes are updated.
GetElementIfAlreadyHeldByLayout(0);
}
}
public void OnLayoutChanging()
{
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
{
_isDataSourceStableResetPending = true;
}
}
public void OnOwnerArranged()
{
if (_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = false;
foreach (var entry in _resetPool)
{
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
// Focused element is getting cleared. Need to figure out semantics on where
// focus should go when the focused element is removed from the data collection.
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
}
_resetPool.Clear();
// Flush the realized indices once the stable reset pool is cleared to start fresh.
InvalidateRealizedIndicesHeldByLayout();
}
}
// We optimize for the case where index is not realized to return null as quickly as we can.
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
// If an index that is realized is requested by the layout, we unfortunately have to walk the
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
// and non-virtualizing hosts.
private Control? GetElementIfAlreadyHeldByLayout(int index)
{
Control? element = null;
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
{
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
int childIndex = virtInfo.Index;
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
if (virtInfo.Index == index)
{
element = child;
// If we have valid first/last indices, we don't have to walk the rest, but if we
// do not, then we keep walking through the entire children collection to get accurate
// indices once.
if (!cachedFirstLastIndicesInvalid)
{
break;
}
}
}
}
}
return element;
}
private Control? GetElementFromUniqueIdResetPool(int index)
{
Control? element = null;
// See if you can get it from the reset pool.
if (_isDataSourceStableResetPending)
{
element = _resetPool.Remove(index);
if (element != null)
{
// Make sure that the index is updated to the current one
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
}
}
return element;
}
private Control? GetElementFromPinnedElements(int index)
{
Control? element = null;
// See if you can find something among the pinned elements.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (virtInfo.Index == index)
{
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break;
}
}
return element;
}
// There are several cases handled here with respect to which element gets returned and when DataContext is modified.
//
// 1. If there is no ItemTemplate:
// 1.1 If data is an Control -> the data is returned
// 1.2 If data is not an Control -> a default DataTemplate is used to fetch element and DataContext is set to data
//
// 2. If there is an ItemTemplate:
// 2.1 If data is not an Control -> Element is fetched from ElementFactory and DataContext is set to the data
// 2.2 If data is an Control:
// 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is
// 2.2.2 If Element returned by the ElementFactory is not the same as the data
// -> Element that is fetched from the ElementFactory is returned and
// DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself
private Control GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
var data = _owner.ItemsSourceView!.GetAt(index);
var providedElementFactory = _owner.ItemTemplateShim;
IElementFactory GetElementFactory()
{
if (providedElementFactory == null)
{
var factory = FuncDataTemplate.Default;
_owner.ItemTemplate = factory;
return _owner.ItemTemplateShim!;
}
return providedElementFactory;
}
Control GetElement()
{
if (providedElementFactory == null)
{
if (data is Control dataAsElement)
{
return dataAsElement;
}
}
var elementFactory = GetElementFactory();
var args = _elementFactoryGetArgs ??= new ElementFactoryGetArgs();
try
{
args.Data = data;
args.Parent = _owner;
args.Index = index;
return elementFactory.GetElement(args);
}
finally
{
args.Data = null;
args.Parent = null;
}
}
var element = GetElement();
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
// Clear flag
virtInfo.MustClearDataContext = false;
if (data != element)
{
// Prepare the element
element.DataContext = data;
virtInfo.MustClearDataContext = true;
}
virtInfo.MoveOwnershipToLayoutFromElementFactory(
index,
/* uniqueId: */
_owner.ItemsSourceView.HasKeyIndexMapping ?
_owner.ItemsSourceView.KeyFromIndex(index) :
string.Empty);
// The view generator is the only provider that prepares the element.
var repeater = _owner;
// Add the element to the children collection here before raising OnElementPrepared so
// that handlers can walk up the tree in case they want to find their IndexPath in the
// nested case.
var children = repeater.Children;
if (element.GetVisualParent() != repeater)
{
children.Add(element);
}
repeater.OnElementPrepared(element, virtInfo);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
return element;
}
private bool ClearElementToUniqueIdResetPool(Control element, VirtualizationInfo virtInfo)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private bool ClearElementToPinnedPool(Control element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
{
bool moveToPinnedPool =
!isClearedDueToCollectionChange && virtInfo.IsPinned;
if (moveToPinnedPool)
{
_pinnedPool.Add(new PinnedElementInfo(element));
virtInfo.MoveOwnershipToPinnedPool();
}
return moveToPinnedPool;
}
private void UpdateFocusedElement()
{
Control? focusedElement = null;
if (TopLevel.GetTopLevel(_owner)?.FocusManager?.GetFocusedElement() is Visual child)
{
var parent = child.GetVisualParent();
var owner = _owner;
// Find out if the focused element belongs to one of our direct
// children.
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
if (repeater == owner &&
child is Control element &&
ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
{
focusedElement = element;
}
break;
}
child = parent;
parent = child.GetVisualParent();
}
}
// If the focused element has changed,
// we need to unpin the old one and pin the new one.
if (_lastFocusedElement != focusedElement)
{
if (_lastFocusedElement != null)
{
UpdatePin(_lastFocusedElement, false /* addPin */);
}
if (focusedElement != null)
{
UpdatePin(focusedElement, true /* addPin */);
}
_lastFocusedElement = focusedElement;
}
}
private void OnFocusChanged(object? sender, RoutedEventArgs e) => UpdateFocusedElement();
private void EnsureEventSubscriptions()
{
if (!_eventsSubscribed)
{
_owner.GotFocus += OnFocusChanged;
_owner.LostFocus += OnFocusChanged;
_eventsSubscribed = true;
}
}
private void UpdateElementIndex(Control element, VirtualizationInfo virtInfo, int index)
{
var oldIndex = virtInfo.Index;
if (oldIndex != index)
{
virtInfo.UpdateIndex(index);
_owner.OnElementIndexChanged(element, oldIndex, index);
}
}
private void InvalidateRealizedIndicesHeldByLayout()
{
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
}
private struct PinnedElementInfo
{
public PinnedElementInfo(Control element)
{
PinnedElement = element;
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
}
public Control PinnedElement { get; }
public VirtualizationInfo VirtualizationInfo { get; }
}
}
}

550
src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs

@ -1,550 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal class ViewportManager
{
private const double CacheBufferPerSideInflationPixelDelta = 40.0;
private readonly ItemsRepeater _owner;
private bool _ensuredScroller;
private IScrollAnchorProvider? _scroller;
private Control? _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Rect _visibleWindow;
private Rect _layoutExtent;
// This is the expected shift by the layout.
private Point _expectedViewportShift;
// This is what is pending and not been accounted for.
// Sometimes the scrolling surface cannot service a shift (for example
// it is already at the top and cannot shift anymore.)
private Point _pendingViewportShift;
// Unshiftable shift amount that this view manager can
// handle on its own to fake it to the layout as if the shift
// actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction.
private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 2.0;
private double _maximumVerticalCacheLength = 2.0;
private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress;
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
private bool _managingViewportDisabled;
private bool _effectiveViewportChangedSubscribed;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
{
_owner = owner;
}
public Control? SuggestedAnchor
{
get
{
// The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
var suggestedAnchor = _makeAnchorElement;
var owner = _owner;
if (suggestedAnchor == null)
{
var anchorElement = _scroller?.CurrentAnchor;
if (anchorElement != null)
{
// We can't simply return anchorElement because, in case of nested Repeaters, it may not
// be a direct child of ours, or even an indirect child. We need to walk up the tree starting
// from anchorElement to figure out what child of ours (if any) to use as the suggested element.
var child = anchorElement;
var parent = child.GetVisualParent() as Control;
while (parent != null)
{
if (parent == owner)
{
suggestedAnchor = child;
break;
}
child = parent;
parent = parent.GetVisualParent() as Control;
}
}
}
return suggestedAnchor;
}
}
public bool HasScroller => _scroller != null;
public Control? MadeAnchor => _makeAnchorElement;
public double HorizontalCacheLength
{
get => _maximumHorizontalCacheLength;
set
{
if (_maximumHorizontalCacheLength != value)
{
ValidateCacheLength(value);
_maximumHorizontalCacheLength = value;
}
}
}
public double VerticalCacheLength
{
get => _maximumVerticalCacheLength;
set
{
if (_maximumVerticalCacheLength != value)
{
ValidateCacheLength(value);
_maximumVerticalCacheLength = value;
}
}
}
public Rect GetLayoutVisibleWindow()
{
var visibleWindow = _visibleWindow;
if (_makeAnchorElement != null)
{
// The anchor is not necessarily laid out yet. Its position should default
// to zero and the layout origin is expected to change once layout is done.
// Until then, we need a window that's going to protect the anchor from
// getting recycled.
visibleWindow = visibleWindow.WithX(0).WithY(0);
}
else if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutRealizationWindow()
{
var realizationWindow = GetLayoutVisibleWindow();
if (HasScroller)
{
realizationWindow = new Rect(
realizationWindow.X - _horizontalCacheBufferPerSide,
realizationWindow.Y - _verticalCacheBufferPerSide,
realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
}
return realizationWindow;
}
public void SetLayoutExtent(Rect extent)
{
_expectedViewportShift = new Point(
_expectedViewportShift.X + _layoutExtent.X - extent.X,
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
// We tolerate viewport imprecisions up to 1 pixel to avoid invalidating layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})",
_owner.Layout?.LayoutId, _expectedViewportShift);
// There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset),
// then we will end up not realizing elements in the visible
// window. To avoid this, we register to layout updated for this layout pass. If we
// get an effective viewport, we know we have a new viewport and we unregister from
// layout updated. If we get the layout updated handler, then we know that the
// scroller was unable to perform the shift and we invalidate measure and unregister
// from the layout updated event.
if (!_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated += OnLayoutUpdated;
_layoutUpdatedSubscribed = true;
}
}
_layoutExtent = extent;
_pendingViewportShift = _expectedViewportShift;
// We just finished a measure pass and have a new extent.
// Let's make sure the scrollers will run its arrange so that they track the anchor.
((Control?)_scroller)?.InvalidateArrange();
}
public Point GetOrigin() => _layoutExtent.TopLeft;
public void OnLayoutChanged(bool isVirtualizing)
{
_managingViewportDisabled = !isVirtualizing;
_layoutExtent = default;
_expectedViewportShift = default;
_pendingViewportShift = default;
_unshiftableShift = default;
if (_managingViewportDisabled && _effectiveViewportChangedSubscribed)
{
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
}
else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed)
{
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
}
public void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
// WinUI registers the element as an anchor candidate here, but I feel that's in error:
// at this point the element has not yet been positioned by the arrange pass so it will
// have its previous position, meaning that when the arrange pass moves it into its new
// position, an incorrect scroll anchoring will occur. Instead signal that it's not yet
// registered as a scroll anchor candidate.
virtInfo.IsRegisteredAsAnchorCandidate = false;
}
public void OnElementCleared(Control element, VirtualizationInfo virtInfo)
{
_scroller?.UnregisterAnchorCandidate(element);
virtInfo.IsRegisteredAsAnchorCandidate = false;
}
public void OnOwnerMeasuring()
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
EnsureScroller();
}
public void OnOwnerArranged()
{
_expectedViewportShift = default;
if (!_managingViewportDisabled)
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
// EnsureScroller();
if (HasScroller)
{
double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
bool continueBuildingCache =
_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
if (continueBuildingCache)
{
_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
_verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
}
}
}
}
private void OnLayoutUpdated(object? sender, EventArgs args)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
if (_managingViewportDisabled)
{
return;
}
// We were expecting a viewport shift but we never got one and we are not going to in this
// layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
// adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
// that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure",
_owner.Layout?.LayoutId,
_pendingViewportShift);
// Assume this is never going to come.
_unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X,
_unshiftableShift.Y + _pendingViewportShift.Y);
_pendingViewportShift = default;
_expectedViewportShift = default;
TryInvalidateMeasure();
}
}
public void OnMakeAnchor(Control? anchor, bool isAnchorOutsideRealizedRange)
{
if (_makeAnchorElement != anchor)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
}
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
{
if (!_managingViewportDisabled)
{
// During the time between a bring into view request and the element coming into view we do not
// want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
// element that is being brought into view. We can do this by making just that element as a potential
// anchor candidate and ensure no other element of this repeater is an anchor candidate.
// Once the layout pass is done and we render the frame, the element will be in frame and we can
// switch back to letting the anchor provider pick a suitable anchor.
// get the targetChild - i.e the immediate child of this repeater that is being brought into view.
// Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((Control)args.TargetObject!);
if (targetChild is null)
{
return;
}
// Make sure that only the target child can be the anchor during the bring into view operation.
if (_scroller is object)
{
foreach (var child in _owner.Children)
{
var info = ItemsRepeater.GetVirtualizationInfo(child);
if (child != targetChild && info.IsRegisteredAsAnchorCandidate)
{
_scroller.UnregisterAnchorCandidate(child);
info.IsRegisteredAsAnchorCandidate = false;
}
}
}
// Register action to go back to how things were before where any child can be the anchor. Here,
// WinUI uses CompositionTarget.Rendering but we don't currently have that, so post an action to
// run *after* rendering has completed (priority needs to be lower than Render as Transformed
// bounds must have been set in order for OnEffectiveViewportChanged to trigger).
if (!_isBringIntoViewInProgress)
{
_isBringIntoViewInProgress = true;
Dispatcher.UIThread.Post(OnCompositionTargetRendering, DispatcherPriority.Loaded);
}
}
}
public void RegisterScrollAnchorCandidate(Control element, VirtualizationInfo virtInfo)
{
if (!virtInfo.IsRegisteredAsAnchorCandidate)
{
_scroller?.RegisterAnchorCandidate(element);
virtInfo.IsRegisteredAsAnchorCandidate = true;
}
}
private Control? GetImmediateChildOfRepeater(Control descendant)
{
var targetChild = descendant;
var parent = (Control?)descendant.GetVisualParent();
while (parent != null && parent != _owner)
{
targetChild = parent;
parent = (Control?)parent.GetVisualParent();
}
if (parent == null)
{
return null;
}
return targetChild;
}
private void OnCompositionTargetRendering()
{
_isBringIntoViewInProgress = false;
_makeAnchorElement = null;
// Undo the anchor deregistrations done by OnBringIntoViewRequested.
if (_scroller is object)
{
foreach (var child in _owner.Children)
{
var info = ItemsRepeater.GetVirtualizationInfo(child);
// The item brought into view is still registered - don't register it more than once.
if (info.IsRealized && info.IsHeldByLayout && !info.IsRegisteredAsAnchorCandidate)
{
_scroller.RegisterAnchorCandidate(child);
info.IsRegisteredAsAnchorCandidate = true;
}
}
}
// HACK: Invalidate measure now that the anchor has been removed so that a layout can be
// done with a proper realization rect. This is a hack not present upstream to try to fix
// https://github.com/microsoft/microsoft-ui-xaml/issues/1422
TryInvalidateMeasure();
}
public void ResetScrollers()
{
if (_scroller is object)
{
foreach (var child in _owner.Children)
{
var info = ItemsRepeater.GetVirtualizationInfo(child);
if (info.IsRegisteredAsAnchorCandidate)
{
_scroller.UnregisterAnchorCandidate(child);
info.IsRegisteredAsAnchorCandidate = false;
}
}
_scroller = null;
}
_owner.EffectiveViewportChanged -= OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = false;
_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout?.LayoutId);
UpdateViewport(e.EffectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.Width == 0 && _visibleWindow.Height == 0)
{
// We got cleared.
_layoutExtent = default;
}
// We got a new viewport, we dont need to wait for layout updated anymore to
// see if our request for a pending shift was handled.
if (_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
}
}
private void EnsureScroller()
{
if (!_ensuredScroller)
{
ResetScrollers();
var parent = _owner.GetVisualParent();
while (parent != null)
{
if (parent is IScrollAnchorProvider scroller)
{
_scroller = scroller;
break;
}
parent = parent.GetVisualParent();
}
if (!_managingViewportDisabled)
{
_owner.EffectiveViewportChanged += OnEffectiveViewportChanged;
_effectiveViewportChangedSubscribed = true;
}
_ensuredScroller = true;
}
}
private void UpdateViewport(Rect viewport)
{
var currentVisibleWindow = viewport;
var previousVisibleWindow = _visibleWindow;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Effective Viewport: ({Before})->({After})",
_owner.Layout?.LayoutId,
previousVisibleWindow,
viewport);
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout?.LayoutId);
// We got cleared.
_visibleWindow = default;
}
else
{
_visibleWindow = currentVisibleWindow;
}
if (_visibleWindow != previousVisibleWindow)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Used Viewport: ({Before})->({After})",
_owner.Layout?.LayoutId,
previousVisibleWindow,
currentVisibleWindow);
TryInvalidateMeasure();
}
}
private static void ValidateCacheLength(double cacheLength)
{
if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
{
throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
}
}
private void TryInvalidateMeasure()
{
// Don't invalidate measure if we have an invalid window.
if (_visibleWindow.Width != 0 || _visibleWindow.Height != 0)
{
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles.
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout?.LayoutId);
_owner.InvalidateMeasure();
}
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)
{
Scroller = scroller;
}
public ScrollViewer Scroller { get; }
}
};
}

119
src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs

@ -1,119 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
internal enum ElementOwner
{
// All elements are originally owned by the view generator.
ElementFactory,
// Ownership is transferred to the layout when it calls GetElement.
Layout,
// Ownership is transferred to the pinned pool if the element is cleared (outside of
// a 'remove' collection change of course).
PinnedPool,
// Ownership is transfered to the reset pool if the element is cleared by a reset and
// the data source supports unique ids.
UniqueIdResetPool,
// Ownership is transfered to the animator if the element is cleared due to a
// 'remove'-like collection change.
Animator
}
internal class VirtualizationInfo
{
private int _pinCounter;
public Rect ArrangeBounds { get; set; }
public bool AutoRecycleCandidate { get; set; }
public int Index { get; private set; }
public bool IsPinned => _pinCounter > 0;
public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; }
public bool IsRegisteredAsAnchorCandidate { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string? UniqueId { get; private set; }
public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
{
Owner = ElementOwner.Layout;
Index = index;
UniqueId = uniqueId;
}
public void MoveOwnershipToLayoutFromUniqueIdResetPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToLayoutFromPinnedPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToElementFactory()
{
Owner = ElementOwner.ElementFactory;
_pinCounter = 0;
Index = -1;
UniqueId = string.Empty;
ArrangeBounds = ItemsRepeater.InvalidRect;
}
public void MoveOwnershipToUniqueIdResetPoolFromLayout()
{
Owner = ElementOwner.UniqueIdResetPool;
// Keep the pinCounter the same. If the container survives the reset
// it can go on being pinned as if nothing happened.
}
public void MoveOwnershipToAnimator()
{
// During a unique id reset, some elements might get removed.
// Their ownership will go from the UniqueIdResetPool to the Animator.
// The common path though is for ownership to go from Layout to Animator.
Owner = ElementOwner.Animator;
Index = -1;
_pinCounter = 0;
}
public void MoveOwnershipToPinnedPool()
{
Owner = ElementOwner.PinnedPool;
}
public int AddPin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't pin an unrealized element.");
}
return ++_pinCounter;
}
public int RemovePin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't unpin an unrealized element.");
}
if (!IsPinned)
{
throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
}
return --_pinCounter;
}
public void UpdateIndex(int newIndex) => Index = newIndex;
}
}

219
src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs

@ -1,219 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using Avalonia.Utilities;
namespace Avalonia.Layout
{
/// <summary>
/// Represents the base class for an object that sizes and arranges child elements for a host.
/// </summary>
public abstract class AttachedLayout : AvaloniaObject
{
public string? LayoutId { get; set; }
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public event EventHandler? MeasureInvalidated;
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> MeasureInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.MeasureInvalidated += h,
(s, h) => s.MeasureInvalidated -= h);
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public event EventHandler? ArrangeInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> ArrangeInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.ArrangeInvalidated += h,
(s, h) => s.ArrangeInvalidated -= h);
/// <summary>
/// Initializes any per-container state the layout requires when it is attached to an
/// <see cref="Layoutable"/> container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <remarks>
/// Container elements that support attached layouts should call this method when a layout
/// instance is first assigned. The container is expected to give the attached layout
/// instance a way to store and retrieve any per-container state by way of the provided
/// context. It is also the responsibility of the container to not reuse the context, or
/// otherwise expose the state from one layout to another.
///
/// When an attached layout is removed the container should release any reference to the
/// layout state it stored.
///
/// Override <see cref="NonVirtualizingLayout.InitializeForContextCore"/> or
/// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
/// this method in a derived class.
/// </remarks>
public void InitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.InitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Removes any state the layout previously stored on the ILayoutable container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
public void UninitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.UninitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Suggests a DesiredSize for a container element. A container element that supports
/// attached layouts should call this method from their own MeasureOverride implementations
/// to form a recursive layout update. The attached layout is expected to call the Measure
/// for each of the container’s ILayoutable children.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="availableSize">
/// The available space that a container can allocate to a child object. A child object can
/// request a larger space than what is available; the provided size might be accommodated
/// if scrolling or other resize behavior is possible in that particular container.
/// </param>
/// <returns></returns>
public Size Measure(LayoutContext context, Size availableSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Positions child elements and determines a size for a container UIElement. Container
/// elements that support attached layouts should call this method from their layout
/// override implementations to form a recursive layout update.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="finalSize">
/// The final size that the container computes for the child in layout.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
public Size Arrange(LayoutContext context, Size finalSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
/// this layout.
/// </summary>
protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty);
/// <summary>
/// Invalidates the arrange state (layout) for all UIElement containers that reference this
/// layout. After the invalidation, the UIElement will have its layout updated, which
/// occurs asynchronously.
/// </summary>
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
private static VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
{
if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext;
}
else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext.GetVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
{
if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext;
}
else if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext.GetNonVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
}
}

463
src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs

@ -1,463 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Layout.Utils;
using Avalonia.Logging;
namespace Avalonia.Layout
{
internal class ElementManager
{
private readonly List<Layoutable?> _realizedElements = new List<Layoutable?>();
private readonly List<Rect> _realizedElementLayoutBounds = new List<Rect>();
private int _firstRealizedDataIndex;
private VirtualizingLayoutContext? _context;
private bool IsVirtualizingContext
{
get
{
if (_context != null)
{
var rect = _context.RealizationRect;
bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
return !hasInfiniteSize;
}
return false;
}
}
public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext;
public void OnBeginMeasure(ScrollOrientation orientation)
{
if (_context != null)
{
if (IsVirtualizingContext)
{
// We proactively clear elements laid out outside of the realization
// rect so that they are available for reuse during the current
// measure pass.
// This is useful during fast panning scenarios in which the realization
// window is constantly changing and we want to reuse elements from
// the end that's opposite to the panning direction.
DiscardElementsOutsideWindow(_context.RealizationRect, orientation);
}
else
{
// If we are initialized with a non-virtualizing context, make sure that
// we have enough space to hold the bounds for all the elements.
int count = _context.ItemCount;
if (_realizedElementLayoutBounds.Count != count)
{
// Make sure there is enough space for the bounds.
// Note: We could optimize when the count becomes smaller, but keeping
// it always up to date is the simplest option for now.
_realizedElementLayoutBounds.Resize(count, default);
}
}
}
}
public int GetRealizedElementCount()
{
return IsVirtualizingContext ? _realizedElements.Count : _context!.ItemCount;
}
public Layoutable GetAt(int realizedIndex)
{
Layoutable? element;
if (IsVirtualizingContext)
{
element = _realizedElements[realizedIndex];
if (element == null)
{
// Sentinel. Create the element now since we need it.
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex);
element = _context!.GetOrCreateElementAt(
dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
_realizedElements[realizedIndex] = element;
}
}
else
{
// realizedIndex and dataIndex are the same (everything is realized)
element = _context!.GetOrCreateElementAt(
realizedIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
}
return element;
}
public void Add(Layoutable element, int dataIndex)
{
if (_realizedElements.Count == 0)
{
_firstRealizedDataIndex = dataIndex;
}
_realizedElements.Add(element);
_realizedElementLayoutBounds.Add(default);
}
public void Insert(int realizedIndex, int dataIndex, Layoutable? element)
{
if (realizedIndex == 0)
{
_firstRealizedDataIndex = dataIndex;
}
_realizedElements.Insert(realizedIndex, element);
// Set bounds to an invalid rect since we do not know it yet.
_realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1));
}
public void ClearRealizedRange(int realizedIndex, int count)
{
for (int i = 0; i < count; i++)
{
// Clear from the edges so that ItemsRepeater can optimize on maintaining
// realized indices without walking through all the children every time.
int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i;
var elementRef = _realizedElements[index];
if (elementRef != null)
{
_context!.RecycleElement(elementRef);
}
}
int endIndex = realizedIndex + count;
_realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex);
_realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex);
if (realizedIndex == 0)
{
_firstRealizedDataIndex = _realizedElements.Count == 0 ?
-1 : _firstRealizedDataIndex + count;
}
}
public void DiscardElementsOutsideWindow(bool forward, int startIndex)
{
// Remove layout elements that are outside the realized range.
if (IsDataIndexRealized(startIndex))
{
int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex);
if (forward)
{
ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex);
}
else
{
ClearRealizedRange(0, rangeIndex + 1);
}
}
}
public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount());
public Rect GetLayoutBoundsForDataIndex(int dataIndex)
{
int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
return _realizedElementLayoutBounds[realizedIndex];
}
public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds)
{
int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
_realizedElementLayoutBounds[realizedIndex] = bounds;
}
public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex];
public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds)
{
_realizedElementLayoutBounds[realizedIndex] = bounds;
}
public bool IsDataIndexRealized(int index)
{
if (IsVirtualizingContext)
{
int realizedCount = GetRealizedElementCount();
return
realizedCount > 0 &&
GetDataIndexFromRealizedRangeIndex(0) <= index &&
GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index;
}
else
{
// Non virtualized - everything is realized
return index >= 0 && index < _context!.ItemCount;
}
}
public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context!.ItemCount;
public Layoutable? GetRealizedElement(int dataIndex)
{
return IsVirtualizingContext ?
GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) :
_context!.GetOrCreateElementAt(
dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
}
public void EnsureElementRealized(bool forward, int dataIndex, string? layoutId)
{
if (IsDataIndexRealized(dataIndex) == false)
{
var element = _context!.GetOrCreateElementAt(
dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
if (forward)
{
Add(element, dataIndex);
}
else
{
Insert(0, dataIndex, element);
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex);
}
}
public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow)
{
bool intersects = false;
if (_realizedElementLayoutBounds.Count > 0)
{
var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
var effectiveOrientation = scrollOrientationSameAsFlow ?
(orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) :
orientation;
var windowStart = effectiveOrientation == ScrollOrientation.Vertical ? window.Y : window.X;
var windowEnd = effectiveOrientation == ScrollOrientation.Vertical ? window.Y + window.Height : window.X + window.Width;
var firstElementStart = effectiveOrientation == ScrollOrientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
var lastElementEnd = effectiveOrientation == ScrollOrientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width;
intersects =
firstElementStart <= windowEnd &&
lastElementEnd >= windowStart;
}
return intersects;
}
public void DataSourceChanged(object? source, NotifyCollectionChangedEventArgs args)
{
if (_realizedElements.Count > 0)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
OnItemsAdded(args.NewStartingIndex, args.NewItems!.Count);
}
break;
case NotifyCollectionChangedAction.Replace:
{
int oldSize = args.OldItems!.Count;
int newSize = args.NewItems!.Count;
int oldStartIndex = args.OldStartingIndex;
int newStartIndex = args.NewStartingIndex;
if (oldSize == newSize &&
oldStartIndex == newStartIndex &&
IsDataIndexRealized(oldStartIndex) &&
IsDataIndexRealized(oldStartIndex + oldSize - 1))
{
// Straight up replace of n items within the realization window.
// Removing and adding might causes us to lose the anchor causing us
// to throw away all containers and start from scratch.
// Instead, we can just clear those items and set the element to
// null (sentinel) and let the next measure get new containers for them.
var startRealizedIndex = GetRealizedRangeIndexFromDataIndex(oldStartIndex);
for (int realizedIndex = startRealizedIndex; realizedIndex < startRealizedIndex + oldSize; realizedIndex++)
{
var elementRef = _realizedElements[realizedIndex];
if (elementRef != null)
{
_context!.RecycleElement(elementRef);
_realizedElements[realizedIndex] = null;
}
}
}
else
{
OnItemsRemoved(oldStartIndex, oldSize);
OnItemsAdded(newStartIndex, newSize);
}
}
break;
// Remove clear all realized elements just to align the begavior
// with ViewManager which resets realized item indices to defaults.
// Freeing only removed items causes wrong indices to be stored
// in virtualized info of items under some circumstances.
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
ClearRealizedRange();
break;
case NotifyCollectionChangedAction.Move:
int size = args.OldItems != null ? args.OldItems.Count : 1;
OnItemsRemoved(args.OldStartingIndex, size);
OnItemsAdded(args.NewStartingIndex, size);
break;
}
}
}
public int GetElementDataIndex(Layoutable suggestedAnchor)
{
var it = _realizedElements.IndexOf(suggestedAnchor);
return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1;
}
public int GetDataIndexFromRealizedRangeIndex(int rangeIndex)
{
return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex;
}
private int GetRealizedRangeIndexFromDataIndex(int dataIndex)
{
return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex;
}
private void DiscardElementsOutsideWindow(in Rect window, ScrollOrientation orientation)
{
// The following illustration explains the cutoff indices.
// We will clear all the realized elements from both ends
// up to the corresponding cutoff index.
// '-' means the element is outside the cutoff range.
// '*' means the element is inside the cutoff range and will be cleared.
//
// Window:
// |______________________________|
// Realization range:
// |*****----------------------------------*********|
// | |
// frontCutoffIndex backCutoffIndex
//
// Note that we tolerate at most one element outside of the window
// because the FlowLayoutAlgorithm.Generate routine stops *after*
// it laid out an element outside the realization window.
// This is also convenient because it protects the anchor
// during a BringIntoView operation during which the anchor may
// not be in the realization window (in fact, the realization window
// might be empty if the BringIntoView is issued before the first
// layout pass).
int realizedRangeSize = GetRealizedElementCount();
int frontCutoffIndex = -1;
int backCutoffIndex = realizedRangeSize;
for (int i = 0;
i < realizedRangeSize &&
!Intersects(window, _realizedElementLayoutBounds[i], orientation);
++i)
{
++frontCutoffIndex;
}
for (int i = realizedRangeSize - 1;
i >= 0 &&
!Intersects(window, _realizedElementLayoutBounds[i], orientation);
--i)
{
--backCutoffIndex;
}
if (backCutoffIndex < realizedRangeSize - 1)
{
ClearRealizedRange(backCutoffIndex + 1, realizedRangeSize - backCutoffIndex - 1);
}
if (frontCutoffIndex > 0)
{
ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount()));
}
}
private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation)
{
var lhsStart = orientation == ScrollOrientation.Vertical ? lhs.Y : lhs.X;
var lhsEnd = orientation == ScrollOrientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
var rhsStart = orientation == ScrollOrientation.Vertical ? rhs.Y : rhs.X;
var rhsEnd = orientation == ScrollOrientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width;
return lhsEnd >= rhsStart && lhsStart <= rhsEnd;
}
private void OnItemsAdded(int index, int count)
{
// Using the old indices here (before it was updated by the collection change)
// if the insert data index is between the first and last realized data index, we need
// to insert items.
int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1;
int newStartingIndex = index;
if (newStartingIndex >= _firstRealizedDataIndex &&
newStartingIndex <= lastRealizedDataIndex)
{
// Inserted within the realized range
int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex;
for (int i = 0; i < count; i++)
{
// Insert null (sentinel) here instead of an element, that way we dont
// end up creating a lot of elements only to be thrown out in the next layout.
int insertRangeIndex = insertRangeStartIndex + i;
int dataIndex = newStartingIndex + i;
// This is to keep the contiguousness of the mapping
Insert(insertRangeIndex, dataIndex, null);
}
}
else if (index <= _firstRealizedDataIndex)
{
// Items were inserted before the realized range.
// We need to update m_firstRealizedDataIndex;
_firstRealizedDataIndex += count;
}
}
private void OnItemsRemoved(int index, int count)
{
int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1;
int startIndex = Math.Max(_firstRealizedDataIndex, index);
int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex);
if (endIndex >= startIndex)
{
ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1);
}
if (removeAffectsFirstRealizedDataIndex &&
_firstRealizedDataIndex != -1)
{
_firstRealizedDataIndex -= count;
}
}
}
}

767
src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs

@ -1,767 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Specialized;
using Avalonia.Logging;
namespace Avalonia.Layout
{
internal class FlowLayoutAlgorithm
{
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
private readonly ElementManager _elementManager = new ElementManager();
private Size _lastAvailableSize;
private double _lastItemSpacing;
private bool _collectionChangePending;
private VirtualizingLayoutContext? _context;
private IFlowLayoutAlgorithmDelegates? _algorithmCallbacks;
private Rect _lastExtent;
private int _firstRealizedDataIndexInsideRealizationWindow = -1;
private int _lastRealizedDataIndexInsideRealizationWindow = -1;
// If the scroll orientation is the same as the follow orientation
// we will only have one line since we will never wrap. In that case
// we do not want to align the line. We could potentially switch the
// meaning of line alignment in this case, but I'll hold off on that
// feature until someone asks for it - This is not a common scenario
// anyway.
private bool _scrollOrientationSameAsFlow;
public Rect LastExtent => _lastExtent;
private bool IsVirtualizingContext
{
get
{
if (_context != null)
{
var rect = _context.RealizationRect;
bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
return !hasInfiniteSize;
}
return false;
}
}
private Rect RealizationRect => IsVirtualizingContext ? _context!.RealizationRect : new Rect(Size.Infinity);
public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
_algorithmCallbacks = callbacks;
_context = context;
_elementManager.SetContext(context);
}
public void UninitializeForContext(VirtualizingLayoutContext context)
{
if (IsVirtualizingContext)
{
// This layout is about to be detached. Let go of all elements
// being held and remove the layout state from the context.
_elementManager.ClearRealizedRange();
}
context.LayoutState = null;
}
public Size Measure(
Size availableSize,
VirtualizingLayoutContext context,
bool isWrapping,
double minItemSpacing,
double lineSpacing,
int maxItemsPerLine,
ScrollOrientation orientation,
bool disableVirtualization,
string? layoutId)
{
_orientation.ScrollOrientation = orientation;
// If minor size is infinity, there is only one line and no need to align that line.
_scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
var realizationRect = RealizationRect;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: MeasureLayout Realization({Rect})",
layoutId,
realizationRect);
var suggestedAnchorIndex = _context!.RecommendedAnchorIndex;
if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
{
var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
if (!anchorRealized)
{
MakeAnchor(_context, suggestedAnchorIndex, availableSize);
}
}
_elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired())
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Reflow Pass", layoutId);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
}
RaiseLineArranged();
_collectionChangePending = false;
_lastExtent = EstimateExtent(availableSize, layoutId);
SetLayoutOrigin();
return new Size(_lastExtent.Width, _lastExtent.Height);
}
public Size Arrange(
Size finalSize,
VirtualizingLayoutContext context,
bool isWrapping,
LineAlignment lineAlignment,
string? layoutId)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: ArrangeLayout", layoutId);
ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId);
return new Size(
Math.Max(finalSize.Width, _lastExtent.Width),
Math.Max(finalSize.Height, _lastExtent.Height));
}
public void OnItemsSourceChanged(
object? source,
NotifyCollectionChangedEventArgs args,
VirtualizingLayoutContext context)
{
_elementManager.DataSourceChanged(source, args);
_collectionChangePending = true;
}
public Size MeasureElement(
Layoutable element,
int index,
Size availableSize,
VirtualizingLayoutContext context)
{
var measureSize = _algorithmCallbacks!.Algorithm_GetMeasureSize(index, availableSize, context);
element.Measure(measureSize);
var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context);
_algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context);
return provisionalArrangeSize;
}
private int GetAnchorIndex(
Size availableSize,
bool isWrapping,
double minItemSpacing,
string? layoutId)
{
int anchorIndex = -1;
var anchorPosition= new Point();
var context = _context;
if (!IsVirtualizingContext)
{
// Non virtualizing host, start generating from the element 0
anchorIndex = context!.ItemCount > 0 ? 0 : -1;
}
else
{
bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow);
// Item spacing and size in non-virtualizing direction change can cause elements to reflow
// and get a new column position. In that case we need the anchor to be positioned in the
// correct column.
bool needAnchorColumnRevaluation = isWrapping && (
_orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) ||
_lastItemSpacing != minItemSpacing ||
_collectionChangePending);
var suggestedAnchorIndex = _context!.RecommendedAnchorIndex;
var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 &&
_elementManager.IsDataIndexRealized(suggestedAnchorIndex);
if (isAnchorSuggestionValid)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex);
anchorIndex = _algorithmCallbacks!.Algorithm_GetAnchorForTargetElement(
suggestedAnchorIndex,
availableSize,
context!).Index;
if (_elementManager.IsDataIndexRealized(anchorIndex))
{
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
if (needAnchorColumnRevaluation)
{
// We were provided a valid anchor, but its position might be incorrect because for example it is in
// the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position
// to start at 0.
anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
}
else
{
anchorPosition = new Point(anchorBounds.X, anchorBounds.Y);
}
}
else if (anchorIndex >= 0)
{
// It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index
// which is not in the realized range. Eg. insert one item at index 0 for a grid layout.
// SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the
// realized range yet. In this case we realize the gap between the target anchor and the suggested anchor.
int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i)
{
_elementManager.EnsureElementRealized(false /*forward*/, i, layoutId);
}
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex);
anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
}
}
else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
{
if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); }
if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Disconnected Window", layoutId); }
// The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
// but not the visible window. In that situation, we still need to produce a valid anchor.
var anchorInfo = _algorithmCallbacks!.Algorithm_GetAnchorForRealizationRect(availableSize, context!);
anchorIndex = anchorInfo.Index;
anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset);
}
else
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId);
// No suggestion - just pick first in realized range
anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
}
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex);
_firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
if (_elementManager.IsIndexValidInData(anchorIndex))
{
if (!_elementManager.IsDataIndexRealized(anchorIndex))
{
// Disconnected, throw everything and create new anchor
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId);
_elementManager.ClearRealizedRange();
var anchor = _context!.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
_elementManager.Add(anchor, anchorIndex);
}
var anchorElement = _elementManager.GetRealizedElement(anchorIndex);
var desiredSize = MeasureElement(anchorElement!, anchorIndex, availableSize, _context!);
var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
_elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})",
layoutId,
anchorIndex,
layoutBounds);
}
else
{
// Throw everything away
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Anchor index is not valid - throwing away all realized elements",
layoutId);
_elementManager.ClearRealizedRange();
}
// TODO: Perhaps we can track changes in the property setter
_lastAvailableSize = availableSize;
_lastItemSpacing = minItemSpacing;
return anchorIndex;
}
private void Generate(
GenerateDirection direction,
int anchorIndex,
Size availableSize,
double minItemSpacing,
double lineSpacing,
int maxItemsPerLine,
bool disableVirtualization,
string? layoutId)
{
if (anchorIndex != -1)
{
int step = (direction == GenerateDirection.Forward) ? 1 : -1;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Generating {Direction} from anchor {Anchor}",
layoutId,
direction,
anchorIndex);
int previousIndex = anchorIndex;
int currentIndex = anchorIndex + step;
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
var lineOffset = _orientation.MajorStart(anchorBounds);
var lineMajorSize = _orientation.MajorSize(anchorBounds);
var countInLine = 1;
int count = 0;
bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) &&
(disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
{
// Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);
var currentElement = _elementManager.GetRealizedElement(currentIndex);
var desiredSize = MeasureElement(currentElement!, currentIndex, availableSize, _context!);
++count;
// Lay it out.
var previousElement = _elementManager.GetRealizedElement(previousIndex);
var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height);
var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex);
if (direction == GenerateDirection.Forward)
{
double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
if (countInLine >= maxItemsPerLine || _algorithmCallbacks!.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// No more space in this row. wrap to next row.
_orientation.SetMinorStart(ref currentBounds, 0);
_orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing);
if (lineNeedsReposition)
{
// reposition the previous line (countInLine items)
for (int i = 0; i < countInLine; i++)
{
var dataIndex = currentIndex - 1 - i;
var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
_orientation.SetMajorSize(ref bounds, lineMajorSize);
_elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
}
}
// Setup for next line.
lineMajorSize = _orientation.MajorSize(currentBounds);
lineOffset = _orientation.MajorStart(currentBounds);
lineNeedsReposition = false;
countInLine = 1;
}
else
{
// More space is available in this row.
_orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing);
_orientation.SetMajorStart(ref currentBounds, lineOffset);
lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
countInLine++;
}
}
else
{
// Backward
double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
if (countInLine >= maxItemsPerLine || _algorithmCallbacks!.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// Does not fit, wrap to the previous row
var availableSizeMinor = _orientation.Minor(availableSize);
_orientation.SetMinorStart(ref currentBounds, !double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0);
_orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing);
if (lineNeedsReposition)
{
var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1));
// reposition the previous line (countInLine items)
for (int i = 0; i < countInLine; i++)
{
var dataIndex = currentIndex + 1 + i;
if (dataIndex != anchorIndex)
{
var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
_orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
_orientation.SetMajorSize(ref bounds, lineMajorSize);
_elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})",
layoutId,
dataIndex,
bounds);
}
}
}
// Setup for next line.
lineMajorSize = _orientation.MajorSize(currentBounds);
lineOffset = _orientation.MajorStart(currentBounds);
lineNeedsReposition = false;
countInLine = 1;
}
else
{
// Fits in this row. put it in the previous position
_orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing);
_orientation.SetMajorStart(ref currentBounds, lineOffset);
lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
countInLine++;
}
}
_elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).",
layoutId,
currentIndex,
currentBounds);
previousIndex = currentIndex;
currentIndex += step;
}
// If we did not reach the top or bottom of the extent, we realized one
// extra item before we knew we were outside the realization window. Do not
// account for that element in the indices inside the realization window.
if (direction == GenerateDirection.Forward)
{
int dataCount = _context!.ItemCount;
_lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
_lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
}
else
{
int dataCount = _context!.ItemCount;
_firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
_firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
}
_elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
}
}
private void MakeAnchor(
VirtualizingLayoutContext context,
int index,
Size availableSize)
{
_elementManager.ClearRealizedRange();
// FlowLayout requires that the anchor is the first element in the row.
var internalAnchor = _algorithmCallbacks!.Algorithm_GetAnchorForTargetElement(index, availableSize, context);
// No need to set the position of the anchor.
// (0,0) is fine for now since the extent can
// grow in any direction.
for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex)
{
var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context));
_elementManager.Add(element, dataIndex);
}
}
private bool IsReflowRequired()
{
// If first element is realized and is not at the very beginning we need to reflow.
return
_elementManager.GetRealizedElementCount() > 0 &&
_elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 &&
_orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0;
}
private bool ShouldContinueFillingUpSpace(
int index,
GenerateDirection direction)
{
bool shouldContinue = false;
if (!IsVirtualizingContext)
{
shouldContinue = true;
}
else
{
var realizationRect = _context!.RealizationRect;
var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index);
var elementMajorStart = _orientation.MajorStart(elementBounds);
var elementMajorEnd = _orientation.MajorEnd(elementBounds);
var rectMajorStart = _orientation.MajorStart(realizationRect);
var rectMajorEnd = _orientation.MajorEnd(realizationRect);
var elementMinorStart = _orientation.MinorStart(elementBounds);
var elementMinorEnd = _orientation.MinorEnd(elementBounds);
var rectMinorStart = _orientation.MinorStart(realizationRect);
var rectMinorEnd = _orientation.MinorEnd(realizationRect);
// Ensure that both minor and major directions are taken into consideration so that if the scrolling direction
// is the same as the flow direction we still stop at the end of the viewport rectangle.
shouldContinue =
(direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) ||
(direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart);
}
return shouldContinue;
}
private Rect EstimateExtent(Size availableSize, string? layoutId)
{
Layoutable? firstRealizedElement = null;
Rect firstBounds = new Rect();
Layoutable? lastRealizedElement = null;
Rect lastBounds = new Rect();
int firstDataIndex = -1;
int lastDataIndex = -1;
if (_elementManager.GetRealizedElementCount() > 0)
{
firstRealizedElement = _elementManager.GetAt(0);
firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
int last = _elementManager.GetRealizedElementCount() - 1;
lastRealizedElement = _elementManager.GetAt(last);
lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last);
lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last);
}
Rect extent = _algorithmCallbacks!.Algorithm_GetExtent(
availableSize,
_context!,
firstRealizedElement,
firstDataIndex,
firstBounds,
lastRealizedElement,
lastDataIndex,
lastBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Extent: ({Bounds})", layoutId, extent);
return extent;
}
private void RaiseLineArranged()
{
var realizationRect = RealizationRect;
if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f)
{
int realizedElementCount = _elementManager.GetRealizedElementCount();
if (realizedElementCount > 0)
{
int countInLine = 0;
var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow);
var currentLineOffset = _orientation.MajorStart(previousElementBounds);
var currentLineSize = _orientation.MajorSize(previousElementBounds);
for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++)
{
var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex);
if (_orientation.MajorStart(currentBounds) != currentLineOffset)
{
// Staring a new line
_algorithmCallbacks!.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context!);
countInLine = 0;
currentLineOffset = _orientation.MajorStart(currentBounds);
currentLineSize = 0;
}
currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
countInLine++;
previousElementBounds = currentBounds;
}
// Raise for the last line.
_algorithmCallbacks!.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context!);
}
}
}
private void ArrangeVirtualizingLayout(
Size finalSize,
LineAlignment lineAlignment,
bool isWrapping,
string? layoutId)
{
// Walk through the realized elements one line at a time and
// align them, Then call element.Arrange with the arranged bounds.
int realizedElementCount = _elementManager.GetRealizedElementCount();
if (realizedElementCount > 0)
{
var countInLine = 1;
var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
var currentLineOffset = _orientation.MajorStart(previousElementBounds);
var spaceAtLineStart = _orientation.MinorStart(previousElementBounds);
var spaceAtLineEnd = 0.0;
var currentLineSize = _orientation.MajorSize(previousElementBounds);
for (int i = 1; i < realizedElementCount; i++)
{
var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i);
if (_orientation.MajorStart(currentBounds) != currentLineOffset)
{
spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
spaceAtLineStart = _orientation.MinorStart(currentBounds);
countInLine = 0;
currentLineOffset = _orientation.MajorStart(currentBounds);
currentLineSize = 0;
}
countInLine++; // for current element
currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
previousElementBounds = currentBounds;
}
// Last line - potentially have a property to customize
// aligning the last line or not.
if (countInLine > 0)
{
var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
}
}
}
// Align elements within a line. Note that this does not modify LayoutBounds. So if we get
// repeated measures, the LayoutBounds remain the same in each layout.
private void PerformLineAlignment(
int lineStartIndex,
int countInLine,
double spaceAtLineStart,
double spaceAtLineEnd,
double lineSize,
LineAlignment lineAlignment,
bool isWrapping,
Size finalSize,
string? layoutId)
{
for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
{
var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex);
_orientation.SetMajorSize(ref bounds, lineSize);
if (!_scrollOrientationSameAsFlow)
{
// Note: Space at start could potentially be negative
if (spaceAtLineStart != 0 || spaceAtLineEnd != 0)
{
var totalSpace = spaceAtLineStart + spaceAtLineEnd;
var minorStart = _orientation.MinorStart(bounds);
switch (lineAlignment)
{
case LineAlignment.Start:
{
_orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart);
break;
}
case LineAlignment.End:
{
_orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd);
break;
}
case LineAlignment.Center:
{
_orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2));
break;
}
case LineAlignment.SpaceAround:
{
var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1)));
break;
}
case LineAlignment.SpaceBetween:
{
var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex)));
break;
}
case LineAlignment.SpaceEvenly:
{
var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1)));
break;
}
}
}
}
bounds = bounds.Translate(-_lastExtent.Position);
if (!isWrapping)
{
_orientation.SetMinorSize(
ref bounds,
Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize)));
}
var element = _elementManager.GetAt(rangeIndex);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Arranging element {Index} at ({Bounds})",
layoutId,
_elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex),
bounds);
element.Arrange(bounds);
}
}
private void SetLayoutOrigin()
{
if (IsVirtualizingContext)
{
_context!.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
}
}
public Layoutable? GetElementIfRealized(int dataIndex)
{
if (_elementManager.IsDataIndexRealized(dataIndex))
{
return _elementManager.GetRealizedElement(dataIndex);
}
return null;
}
public bool TryAddElement0(Layoutable element)
{
if (_elementManager.GetRealizedElementCount() == 0)
{
_elementManager.Add(element, 0);
return true;
}
return false;
}
public enum LineAlignment
{
Start,
Center,
End,
SpaceAround,
SpaceBetween,
SpaceEvenly,
}
private enum GenerateDirection
{
Forward,
Backward,
}
}
}

44
src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs

@ -1,44 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Layout
{
internal struct FlowLayoutAnchorInfo
{
public int Index { get; set; }
public double Offset { get; set; }
}
internal interface IFlowLayoutAlgorithmDelegates
{
Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context);
Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context);
bool Algorithm_ShouldBreakLine(int index, double remainingSpace);
FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context);
FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context);
Rect Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
Layoutable? firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
Layoutable? lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds);
void Algorithm_OnElementMeasured(
Layoutable element,
int index,
Size availableSize,
Size measureSize,
Size desiredSize,
Size provisionalArrangeSize,
VirtualizingLayoutContext context);
void Algorithm_OnLineArranged(
int startIndex,
int countInLine,
double lineSize,
VirtualizingLayoutContext context);
}
}

28
src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs

@ -1,28 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Layout
{
/// <summary>
/// Represents the base class for an object that facilitates communication between an attached
/// layout and its host container.
/// </summary>
public class LayoutContext : AvaloniaObject
{
/// <summary>
/// Gets or sets an object that represents the state of a layout.
/// </summary>
public object? LayoutState
{
get => LayoutStateCore;
set => LayoutStateCore = value;
}
/// <summary>
/// Implements the behavior of <see cref="LayoutState"/> in a derived or custom LayoutContext.
/// </summary>
protected virtual object? LayoutStateCore { get; set; }
}
}

45
src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs

@ -1,45 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Layout
{
internal class LayoutContextAdapter : VirtualizingLayoutContext
{
private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
{
_nonVirtualizingContext = nonVirtualizingContext;
}
protected override object? LayoutStateCore
{
get => _nonVirtualizingContext.LayoutState;
set => _nonVirtualizingContext.LayoutState = value;
}
protected override Point LayoutOriginCore
{
get => default;
set
{
if (value != default)
{
throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
}
}
}
protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
protected override Layoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
_nonVirtualizingContext.Children[index];
protected override void RecycleElementCore(Layoutable element) { }
}
}

83
src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs

@ -1,83 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Layout
{
/// <summary>
/// Represents the base class for an object that sizes and arranges child elements for a host
/// and and does not support virtualization.
/// </summary>
/// <remarks>
/// NonVirtualizingLayout is the base class for layouts that do not support virtualization. You
/// can inherit from it to create your own layout.
///
/// A non-virtualizing layout can measure and arrange child elements.
/// </remarks>
public abstract class NonVirtualizingLayout : AttachedLayout
{
/// <summary>
/// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected internal virtual void InitializeForContextCore(LayoutContext context)
{
}
/// <summary>
/// When overridden in a derived class, removes any state the layout previously stored on
/// the ILayoutable container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected internal virtual void UninitializeForContextCore(LayoutContext context)
{
}
/// <summary>
/// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
/// this method to define their own "Measure" pass behavior.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="availableSize">
/// The available size that this object can give to child objects. Infinity can be
/// specified as a value to indicate that the object will size to whatever content is
/// available.
/// </param>
/// <returns>
/// The size that this object determines it needs during layout, based on its calculations
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
/// layout. Classes can override this method to define their own "Arrange" pass behavior.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="finalSize">
/// The final area within the container that this object should use to arrange itself and
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected internal virtual Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize) => finalSize;
}
}

31
src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs

@ -1,31 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
namespace Avalonia.Layout
{
/// <summary>
/// Represents the base class for layout context types that do not support virtualization.
/// </summary>
public abstract class NonVirtualizingLayoutContext : LayoutContext
{
private VirtualizingLayoutContext? _contextAdapter;
/// <summary>
/// Gets the collection of child controls from the container that provides the context.
/// </summary>
public IReadOnlyList<Layoutable> Children => ChildrenCore;
/// <summary>
/// Implements the behavior for getting the return value of <see cref="Children"/> in a
/// derived or custom <see cref="NonVirtualizingLayoutContext"/>.
/// </summary>
protected abstract IReadOnlyList<Layoutable> ChildrenCore { get; }
internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
_contextAdapter ??= new LayoutContextAdapter(this);
}
}

162
src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs

@ -1,162 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Layout
{
public class NonVirtualizingStackLayout : NonVirtualizingLayout
{
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
/// direction of the StackLayout's Orientation.
/// </summary>
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
protected internal override Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize)
{
var extentU = 0.0;
var extentV = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var constraint = isVertical ?
availableSize.WithHeight(double.PositiveInfinity) :
availableSize.WithWidth(double.PositiveInfinity);
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if ((element as Visual)?.IsVisible == false)
{
continue;
}
element.Measure(constraint);
if (isVertical)
{
extentU += element.DesiredSize.Height;
extentV = Math.Max(extentV, element.DesiredSize.Width);
}
else
{
extentU += element.DesiredSize.Width;
extentV = Math.Max(extentV, element.DesiredSize.Height);
}
if (i < childCount - 1)
{
extentU += spacing;
}
}
return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
}
protected internal override Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize)
{
var u = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var bounds = new Rect();
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if ((element as Visual)?.IsVisible == false)
{
continue;
}
bounds = isVertical ?
LayoutVertical(element, u, finalSize) :
LayoutHorizontal(element, u, finalSize);
element.Arrange(bounds);
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
}
return new Size(
Math.Max(finalSize.Width, bounds.Width),
Math.Max(finalSize.Height, bounds.Height));
}
private static Rect LayoutVertical(Layoutable element, double y, Size constraint)
{
var x = 0.0;
var width = element.DesiredSize.Width;
switch (element.HorizontalAlignment)
{
case HorizontalAlignment.Center:
x += (constraint.Width - element.DesiredSize.Width) / 2;
break;
case HorizontalAlignment.Right:
x += constraint.Width - element.DesiredSize.Width;
break;
case HorizontalAlignment.Stretch:
width = constraint.Width;
break;
}
return new Rect(x, y, width, element.DesiredSize.Height);
}
private static Rect LayoutHorizontal(Layoutable element, double x, Size constraint)
{
var y = 0.0;
var height = element.DesiredSize.Height;
switch (element.VerticalAlignment)
{
case VerticalAlignment.Center:
y += (constraint.Height - element.DesiredSize.Height) / 2;
break;
case VerticalAlignment.Bottom:
y += constraint.Height - element.DesiredSize.Height;
break;
case VerticalAlignment.Stretch:
height = constraint.Height;
break;
}
return new Rect(x, y, element.DesiredSize.Width, height);
}
}
}

96
src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs

@ -1,96 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Layout
{
internal enum ScrollOrientation
{
Vertical,
Horizontal,
}
internal class OrientationBasedMeasures
{
public ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical;
public double Major(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Height : size.Width;
public double Minor(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Width : size.Height;
public double MajorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Height : rect.Width;
public double MinorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Width : rect.Height;
public double MajorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Y : rect.X;
public double MinorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.X : rect.Y;
public double MajorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Bottom : rect.Right;
public double MinorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Right : rect.Bottom;
public void SetMajorSize(ref Rect rect, double value)
{
if (ScrollOrientation == ScrollOrientation.Vertical)
{
rect = rect.WithHeight(value);
}
else
{
rect = rect.WithWidth(value);
}
}
public void SetMinorSize(ref Rect rect, double value)
{
if (ScrollOrientation == ScrollOrientation.Vertical)
{
rect = rect.WithWidth(value);
}
else
{
rect = rect.WithHeight(value);
}
}
public void SetMajorStart(ref Rect rect, double value)
{
if (ScrollOrientation == ScrollOrientation.Vertical)
{
rect = rect.WithY(value);
}
else
{
rect = rect.WithX(value);
}
}
public void SetMinorStart(ref Rect rect, double value)
{
if (ScrollOrientation == ScrollOrientation.Vertical)
{
rect = rect.WithX(value);
}
else
{
rect = rect.WithY(value);
}
}
public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize)
{
return ScrollOrientation == ScrollOrientation.Vertical ?
new Rect(minor, major, minorSize, majorSize) :
new Rect(major, minor, majorSize, minorSize);
}
public Point MinorMajorPoint(double minor, double major)
{
return ScrollOrientation == ScrollOrientation.Vertical ?
new Point(minor, major) :
new Point(major, minor);
}
public Size MinorMajorSize(double minor, double major)
{
return ScrollOrientation == ScrollOrientation.Vertical ?
new Size(minor, major) :
new Size(major, minor);
}
}
}

365
src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs

@ -1,365 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Layout
{
/// <summary>
/// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically.
/// </summary>
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
/// <summary>
/// Defines the <see cref="DisableVirtualization"/> property.
/// </summary>
public static readonly StyledProperty<bool> DisableVirtualizationProperty =
AvaloniaProperty.Register<StackLayout, bool>(nameof(DisableVirtualization));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<StackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
StackPanel.SpacingProperty.AddOwner<StackLayout>();
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
/// <summary>
/// Initializes a new instance of the StackLayout class.
/// </summary>
public StackLayout()
{
LayoutId = "StackLayout";
}
/// <summary>
/// Gets or sets a value indicating whether virtualization is disabled on the layout.
/// </summary>
public bool DisableVirtualization
{
get => GetValue(DisableVirtualizationProperty);
set => SetValue(DisableVirtualizationProperty, value);
}
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
/// direction of the StackLayout's Orientation.
/// </summary>
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
internal Rect GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
Layoutable? firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
Layoutable? lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
var stackState = (StackLayoutState)context.LayoutState!;
double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing;
_orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds);
_orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing));
if (itemsCount > 0)
{
if (firstRealized != null)
{
_orientation.SetMajorStart(
ref extent,
_orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize);
var remainingItems = itemsCount - lastRealizedItemIndex - 1;
_orientation.SetMajorSize(
ref extent,
_orientation.MajorEnd(lastRealizedLayoutBounds) -
_orientation.MajorStart(extent) +
(remainingItems * averageElementSize));
}
else
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements",
LayoutId);
}
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}",
LayoutId, extent.Size, averageElementSize);
return extent;
}
internal void OnElementMeasured(
Layoutable element,
int index,
Size availableSize,
Size measureSize,
Size desiredSize,
Size provisionalArrangeSize,
VirtualizingLayoutContext context)
{
if (context is VirtualizingLayoutContext virtualContext)
{
var stackState = (StackLayoutState)virtualContext.LayoutState!;
var provisionalArrangeSizeWinRt = provisionalArrangeSize;
stackState.OnElementMeasured(
index,
_orientation.Major(provisionalArrangeSizeWinRt),
_orientation.Minor(provisionalArrangeSizeWinRt));
}
}
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
int index,
Size availableSize,
VirtualizingLayoutContext context) => availableSize;
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
int index,
Size measureSize,
Size desiredSize,
VirtualizingLayoutContext context)
{
var measureSizeMinor = _orientation.Minor(measureSize);
return _orientation.MinorMajorSize(
!double.IsInfinity(measureSizeMinor) ?
Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) :
_orientation.Minor(desiredSize),
_orientation.Major(desiredSize));
}
bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true;
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context);
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
int targetIndex,
Size availableSize,
VirtualizingLayoutContext context)
{
double offset = double.NaN;
int index = -1;
int itemsCount = context.ItemCount;
if (targetIndex >= 0 && targetIndex < itemsCount)
{
index = targetIndex;
var state = (StackLayoutState)context.LayoutState!;
double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent);
}
return new FlowLayoutAnchorInfo { Index = index, Offset = offset };
}
Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
Layoutable? firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
Layoutable? lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
return GetExtent(
availableSize,
context,
firstRealized,
firstRealizedItemIndex,
firstRealizedLayoutBounds,
lastRealized,
lastRealizedItemIndex,
lastRealizedLayoutBounds);
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(Layoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
{
OnElementMeasured(
element,
index,
availableSize,
measureSize,
desiredSize,
provisionalArrangeSize,
context);
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
{
}
internal FlowLayoutAnchorInfo GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context)
{
int anchorIndex = -1;
double offset = double.NaN;
// Constants
int itemsCount = context.ItemCount;
if (itemsCount > 0)
{
var realizationRect = context.RealizationRect;
var state = (StackLayoutState)context.LayoutState!;
var lastExtent = state.FlowAlgorithm.LastExtent;
double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent);
if (itemsCount > 0 &&
_orientation.MajorSize(realizationRect) >= 0 &&
// MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also,
// note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could
// just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate
// in the navigating direction.
realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
{
anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
}
}
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
if (stackState == null)
{
if (state != null)
{
throw new InvalidOperationException("LayoutState must derive from StackLayoutState.");
}
// Custom deriving layouts could potentially be stateful.
// If that is the case, we will just create the base state required by UniformGridLayout ourselves.
stackState = new StackLayoutState();
}
stackState.InitializeForContext(context, this);
}
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState!;
stackState.UninitializeForContext(context);
}
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
((StackLayoutState)context.LayoutState!).OnMeasureStart();
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
false,
0,
Spacing,
int.MaxValue,
_orientation.ScrollOrientation,
DisableVirtualization,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
}
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
false,
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
return new Size(value.Width, value.Height);
}
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args)
{
GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
// Always invalidate layout to keep the view accurate.
InvalidateLayout();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == OrientationProperty)
{
var orientation = change.GetNewValue<Orientation>();
//Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal ScrollOrientation.
_orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical;
}
InvalidateLayout();
}
private static double GetAverageElementSize(
Size availableSize,
VirtualizingLayoutContext context,
StackLayoutState stackLayoutState)
{
double averageElementSize = 0;
if (context.ItemCount > 0)
{
if (stackLayoutState.TotalElementsMeasured == 0)
{
var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context);
context.RecycleElement(tmpElement);
}
averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured);
}
return averageElementSize;
}
private void InvalidateLayout() => InvalidateMeasure();
private static FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState!).FlowAlgorithm;
}
}

61
src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs

@ -1,61 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Layout
{
/// <summary>
/// Represents the state of a StackLayout.
/// </summary>
public class StackLayoutState
{
private const int BufferSize = 100;
private readonly List<double> _estimationBuffer = new List<double>();
internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
internal double MaxArrangeBounds { get; private set; }
internal int TotalElementsMeasured { get; private set; }
internal double TotalElementSize { get; private set; }
internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
FlowAlgorithm.InitializeForContext(context, callbacks);
if (_estimationBuffer.Count == 0)
{
_estimationBuffer.AddRange(Enumerable.Repeat(0.0, BufferSize));
}
context.LayoutState = this;
}
internal void UninitializeForContext(VirtualizingLayoutContext context)
{
FlowAlgorithm.UninitializeForContext(context);
}
internal void OnElementMeasured(int elementIndex, double majorSize, double minorSize)
{
int estimationBufferIndex = elementIndex % _estimationBuffer.Count;
bool alreadyMeasured = _estimationBuffer[estimationBufferIndex] != 0;
if (!alreadyMeasured)
{
TotalElementsMeasured++;
}
TotalElementSize -= _estimationBuffer[estimationBufferIndex];
TotalElementSize += majorSize;
_estimationBuffer[estimationBufferIndex] = majorSize;
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
}
internal void OnMeasureStart() => MaxArrangeBounds = 0;
}
}

562
src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs

@ -1,562 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Specialized;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Layout
{
/// <summary>
/// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis.
/// </summary>
public enum UniformGridLayoutItemsJustification
{
/// <summary>
/// Items are aligned with the start of the row or column, with extra space at the end.
/// Spacing between items does not change.
/// </summary>
Start = 0,
/// <summary>
/// Items are aligned in the center of the row or column, with extra space at the start and
/// end. Spacing between items does not change.
/// </summary>
Center = 1,
/// <summary>
/// Items are aligned with the end of the row or column, with extra space at the start.
/// Spacing between items does not change.
/// </summary>
End = 2,
/// <summary>
/// Items are aligned so that extra space is added evenly before and after each item.
/// </summary>
SpaceAround = 3,
/// <summary>
/// Items are aligned so that extra space is added evenly between adjacent items. No space
/// is added at the start or end.
/// </summary>
SpaceBetween = 4,
SpaceEvenly = 5,
};
/// <summary>
/// Defines constants that specify how items are sized to fill the available space.
/// </summary>
public enum UniformGridLayoutItemsStretch
{
/// <summary>
/// The item retains its natural size. Use of extra space is determined by the
/// <see cref="UniformGridLayout.ItemsJustification"/> property.
/// </summary>
None = 0,
/// <summary>
/// The item is sized to fill the available space in the non-scrolling direction. Item size
/// in the scrolling direction is not changed.
/// </summary>
Fill = 1,
/// <summary>
/// The item is sized to both fill the available space in the non-scrolling direction and
/// maintain its aspect ratio.
/// </summary>
Uniform = 2,
};
/// <summary>
/// Positions elements sequentially from left to right or top to bottom in a wrapping layout.
/// </summary>
public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
/// <summary>
/// Defines the <see cref="ItemsJustification"/> property.
/// </summary>
public static readonly StyledProperty<UniformGridLayoutItemsJustification> ItemsJustificationProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsJustification>(nameof(ItemsJustification));
/// <summary>
/// Defines the <see cref="ItemsStretch"/> property.
/// </summary>
public static readonly StyledProperty<UniformGridLayoutItemsStretch> ItemsStretchProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsStretch>(nameof(ItemsStretch));
/// <summary>
/// Defines the <see cref="MinColumnSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> MinColumnSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinColumnSpacing));
/// <summary>
/// Defines the <see cref="MinItemHeight"/> property.
/// </summary>
public static readonly StyledProperty<double> MinItemHeightProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemHeight));
/// <summary>
/// Defines the <see cref="MinItemWidth"/> property.
/// </summary>
public static readonly StyledProperty<double> MinItemWidthProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemWidth));
/// <summary>
/// Defines the <see cref="MinRowSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> MinRowSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
/// <summary>
/// Defines the <see cref="MaximumRowsOrColumns"/> property.
/// </summary>
public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MaximumRowsOrColumns));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<UniformGridLayout>();
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
private double _minItemWidth = double.NaN;
private double _minItemHeight = double.NaN;
private double _minRowSpacing;
private double _minColumnSpacing;
private UniformGridLayoutItemsJustification _itemsJustification;
private UniformGridLayoutItemsStretch _itemsStretch;
private int _maximumRowsOrColumns = int.MaxValue;
/// <summary>
/// Initializes a new instance of the <see cref="UniformGridLayout"/> class.
/// </summary>
public UniformGridLayout()
{
LayoutId = "UniformGridLayout";
}
static UniformGridLayout()
{
OrientationProperty.OverrideDefaultValue<UniformGridLayout>(Orientation.Horizontal);
}
/// <summary>
/// Gets or sets a value that indicates how items are aligned on the non-scrolling or non-
/// virtualizing axis.
/// </summary>
/// <value>
/// An enumeration value that indicates how items are aligned. The default is Start.
/// </value>
public UniformGridLayoutItemsJustification ItemsJustification
{
get => GetValue(ItemsJustificationProperty);
set => SetValue(ItemsJustificationProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates how items are sized to fill the available space.
/// </summary>
/// <value>
/// An enumeration value that indicates how items are sized to fill the available space.
/// The default is None.
/// </value>
/// <remarks>
/// This property enables adaptive layout behavior where the items are sized to fill the
/// available space along the non-scrolling axis, and optionally maintain their aspect ratio.
/// </remarks>
public UniformGridLayoutItemsStretch ItemsStretch
{
get => GetValue(ItemsStretchProperty);
set => SetValue(ItemsStretchProperty, value);
}
/// <summary>
/// Gets or sets the minimum space between items on the horizontal axis.
/// </summary>
/// <remarks>
/// The spacing may exceed this minimum value when <see cref="ItemsJustification"/> is set
/// to SpaceEvenly, SpaceAround, or SpaceBetween.
/// </remarks>
public double MinColumnSpacing
{
get => GetValue(MinColumnSpacingProperty);
set => SetValue(MinColumnSpacingProperty, value);
}
/// <summary>
/// Gets or sets the minimum height of each item.
/// </summary>
/// <value>
/// The minimum height (in pixels) of each item. The default is NaN, in which case the
/// height of the first item is used as the minimum.
/// </value>
public double MinItemHeight
{
get => GetValue(MinItemHeightProperty);
set => SetValue(MinItemHeightProperty, value);
}
/// <summary>
/// Gets or sets the minimum width of each item.
/// </summary>
/// <value>
/// The minimum width (in pixels) of each item. The default is NaN, in which case the width
/// of the first item is used as the minimum.
/// </value>
public double MinItemWidth
{
get => GetValue(MinItemWidthProperty);
set => SetValue(MinItemWidthProperty, value);
}
/// <summary>
/// Gets or sets the minimum space between items on the vertical axis.
/// </summary>
/// <remarks>
/// The spacing may exceed this minimum value when <see cref="ItemsJustification"/> is set
/// to SpaceEvenly, SpaceAround, or SpaceBetween.
/// </remarks>
public double MinRowSpacing
{
get => GetValue(MinRowSpacingProperty);
set => SetValue(MinRowSpacingProperty, value);
}
/// <summary>
/// Gets or sets the maximum row or column count.
/// </summary>
public int MaximumRowsOrColumns
{
get => GetValue(MaximumRowsOrColumnsProperty);
set => SetValue(MaximumRowsOrColumnsProperty, value);
}
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing;
internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing;
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
int index,
Size availableSize,
VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
}
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
int index,
Size measureSize,
Size desiredSize,
VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
}
bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0;
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context)
{
Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN);
int anchorIndex = -1;
int itemsCount = context.ItemCount;
var realizationRect = context.RealizationRect;
if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
var lastExtent = gridState.FlowAlgorithm.LastExtent;
var itemsPerLine = Math.Min( // note use of unsigned ints
Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
Math.Max(1u, (uint)_maximumRowsOrColumns));
var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
{
double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
}
}
return new FlowLayoutAnchorInfo
{
Index = anchorIndex,
Offset = _orientation.MajorStart(bounds)
};
}
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
int targetIndex,
Size availableSize,
VirtualizingLayoutContext context)
{
int index = -1;
double offset = double.NaN;
int count = context.ItemCount;
if (targetIndex >= 0 && targetIndex < count)
{
int itemsPerLine = (int)Math.Min( // note use of unsigned ints
Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
Math.Max(1u, _maximumRowsOrColumns));
int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = indexOfFirstInLine;
var state = (UniformGridLayoutState)context.LayoutState!;
offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context));
}
return new FlowLayoutAnchorInfo
{
Index = index,
Offset = offset
};
}
Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
Layoutable? firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
Layoutable? lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize);
int itemsPerLine =
(int)Math.Min( // note use of unsigned ints
Math.Max(1u, !double.IsInfinity(availableSizeMinor)
? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context))
: (uint)itemsCount),
Math.Max(1u, _maximumRowsOrColumns));
double lineSize = GetMajorSizeWithSpacing(context);
if (itemsCount > 0)
{
_orientation.SetMinorSize(
ref extent,
!double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ?
availableSizeMinor :
Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
_orientation.SetMajorSize(
ref extent,
Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
if (firstRealized != null)
{
_orientation.SetMajorStart(
ref extent,
_orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize);
int remainingItems = itemsCount - lastRealizedItemIndex - 1;
_orientation.SetMajorSize(
ref extent,
_orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize);
}
else
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId);
}
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}",
LayoutId, extent.Size, lineSize, itemsPerLine);
return extent;
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(Layoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
{
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
{
}
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var gridState = state as UniformGridLayoutState;
if (gridState == null)
{
if (state != null)
{
throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState.");
}
// Custom deriving layouts could potentially be stateful.
// If that is the case, we will just create the base state required by UniformGridLayout ourselves.
gridState = new UniformGridLayoutState();
}
gridState.InitializeForContext(context, this);
}
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
gridState.UninitializeForContext(context);
}
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
// Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
var gridState = (UniformGridLayoutState)context.LayoutState!;
gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns);
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
true,
MinItemSpacing,
LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation,
false,
LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
// and only use the layout when to clear it when it's done.
gridState.EnsureFirstElementOwnership(context);
return desiredSize;
}
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
true,
(FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
LayoutId);
return new Size(value.Width, value.Height);
}
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args)
{
GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
// Always invalidate layout to keep the view accurate.
InvalidateLayout();
var gridState = (UniformGridLayoutState)context.LayoutState!;
gridState.ClearElementOnDataSourceChange(context, args);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == OrientationProperty)
{
var orientation = change.GetNewValue<Orientation>();
//Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
//i.e. the properties are the inverse of each other.
var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation;
}
else if (change.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = change.GetNewValue<double>();
}
else if (change.Property == MinRowSpacingProperty)
{
_minRowSpacing = change.GetNewValue<double>();
}
else if (change.Property == ItemsJustificationProperty)
{
_itemsJustification = change.GetNewValue<UniformGridLayoutItemsJustification>();
}
else if (change.Property == ItemsStretchProperty)
{
_itemsStretch = change.GetNewValue<UniformGridLayoutItemsStretch>();
}
else if (change.Property == MinItemWidthProperty)
{
_minItemWidth = change.GetNewValue<double>();
}
else if (change.Property == MinItemHeightProperty)
{
_minItemHeight = change.GetNewValue<double>();
}
else if (change.Property == MaximumRowsOrColumnsProperty)
{
_maximumRowsOrColumns = change.GetNewValue<int>();
}
InvalidateLayout();
}
private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
{
var minItemSpacing = MinItemSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState!;
return _orientation.ScrollOrientation == ScrollOrientation.Vertical?
gridState.EffectiveItemWidth + minItemSpacing :
gridState.EffectiveItemHeight + minItemSpacing;
}
private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
{
var lineSpacing = LineSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState!;
return _orientation.ScrollOrientation == ScrollOrientation.Vertical ?
gridState.EffectiveItemHeight + lineSpacing :
gridState.EffectiveItemWidth + lineSpacing;
}
Rect GetLayoutRectForDataIndex(
Size availableSize,
int index,
Rect lastExtent,
VirtualizingLayoutContext context)
{
int itemsPerLine = (int)Math.Min( //note use of unsigned ints
Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))),
Math.Max(1u, _maximumRowsOrColumns));
int rowIndex = (int)(index / itemsPerLine);
int indexInRow = index - (rowIndex * itemsPerLine);
var gridState = (UniformGridLayoutState)context.LayoutState!;
Rect bounds = _orientation.MinorMajorRect(
indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent),
rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent),
_orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight,
_orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth);
return bounds;
}
private void InvalidateLayout() => InvalidateMeasure();
private static FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState!).FlowAlgorithm;
}
}

204
src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs

@ -1,204 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Specialized;
namespace Avalonia.Layout
{
/// <summary>
/// Represents the state of a <see cref="UniformGridLayout"/>.
/// </summary>
public class UniformGridLayoutState
{
// We need to measure the element at index 0 to know what size to measure all other items.
// If FlowlayoutAlgorithm has already realized element 0 then we can use that.
// If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it.
// If we are responsible for clearing element 0 we keep m_cachedFirstElement valid.
// If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm.
private Layoutable? _cachedFirstElement;
internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
internal double EffectiveItemWidth { get; private set; }
internal double EffectiveItemHeight { get; private set; }
internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
FlowAlgorithm.InitializeForContext(context, callbacks);
context.LayoutState = this;
}
internal void UninitializeForContext(VirtualizingLayoutContext context)
{
FlowAlgorithm.UninitializeForContext(context);
if (_cachedFirstElement != null)
{
context.RecycleElement(_cachedFirstElement);
}
}
internal void EnsureElementSize(
Size availableSize,
VirtualizingLayoutContext context,
double layoutItemWidth,
double layoutItemHeight,
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
double minColumnSpacing,
int maxItemsPerLine)
{
if (maxItemsPerLine == 0)
{
maxItemsPerLine = 1;
}
if (context.ItemCount > 0)
{
// If the first element is realized we don't need to cache it or to get it from the context
var realizedElement = FlowAlgorithm.GetElementIfRealized(0);
if (realizedElement != null)
{
realizedElement.Measure(availableSize);
SetSize(realizedElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
_cachedFirstElement = null;
}
else
{
if (_cachedFirstElement == null)
{
// we only cache if we aren't realizing it
_cachedFirstElement = context.GetOrCreateElementAt(
0,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // expensive
}
_cachedFirstElement.Measure(availableSize);
SetSize(_cachedFirstElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
// See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
if (added)
{
_cachedFirstElement = null;
}
}
}
}
private void SetSize(
Layoutable element,
double layoutItemWidth,
double layoutItemHeight,
Size availableSize,
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
double minColumnSpacing,
int maxItemsPerLine)
{
if (maxItemsPerLine == 0)
{
maxItemsPerLine = 1;
}
EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
EffectiveItemHeight = (double.IsNaN(layoutItemHeight) ? element.DesiredSize.Height : layoutItemHeight);
var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;
var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight;
double extraMinorPixelsForEachItem = 0.0;
if (!double.IsInfinity(availableSizeMinor))
{
var numItemsPerColumn = (int)Math.Min(
maxItemsPerLine,
Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing)));
var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing;
var remainingSpace = availableSizeMinor - usedSpace;
extraMinorPixelsForEachItem = (int)(remainingSpace / numItemsPerColumn);
}
if (stretch == UniformGridLayoutItemsStretch.Fill)
{
if (orientation == Orientation.Horizontal)
{
EffectiveItemWidth += extraMinorPixelsForEachItem;
}
else
{
EffectiveItemHeight += extraMinorPixelsForEachItem;
}
}
else if (stretch == UniformGridLayoutItemsStretch.Uniform)
{
var itemSizeMajor = orientation == Orientation.Horizontal ? EffectiveItemHeight : EffectiveItemWidth;
var extraMajorPixelsForEachItem = itemSizeMajor * (extraMinorPixelsForEachItem / itemSizeMinor);
if (orientation == Orientation.Horizontal)
{
EffectiveItemWidth += extraMinorPixelsForEachItem;
EffectiveItemHeight += extraMajorPixelsForEachItem;
}
else
{
EffectiveItemHeight += extraMinorPixelsForEachItem;
EffectiveItemWidth += extraMajorPixelsForEachItem;
}
}
}
internal void EnsureFirstElementOwnership(VirtualizingLayoutContext context)
{
if (_cachedFirstElement != null && FlowAlgorithm.GetElementIfRealized(0) != null)
{
// We created the element, but then flowlayout algorithm took ownership, so we can clear it and
// let flowlayout algorithm do its thing.
context.RecycleElement(_cachedFirstElement);
_cachedFirstElement = null;
}
}
internal void ClearElementOnDataSourceChange(
VirtualizingLayoutContext context,
NotifyCollectionChangedEventArgs args)
{
if (_cachedFirstElement != null)
{
bool shouldClear = false;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
shouldClear = args.NewStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Replace:
shouldClear = args.NewStartingIndex == 0 || args.OldStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Remove:
shouldClear = args.OldStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Reset:
shouldClear = true;
break;
case NotifyCollectionChangedAction.Move:
throw new NotImplementedException();
}
if (shouldClear)
{
context.RecycleElement(_cachedFirstElement);
_cachedFirstElement = null;
}
}
}
}
}

27
src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs

@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Layout.Utils
{
internal static class ListUtils
{
public static void Resize<T>(this List<T> list, int size, T value)
{
int cur = list.Count;
if (size < cur)
{
list.RemoveRange(size, cur - size);
}
else if (size > cur)
{
if (size > list.Capacity)
{
list.Capacity = size;
}
list.AddRange(Enumerable.Repeat(value, size - cur));
}
}
}
}

37
src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs

@ -1,37 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Avalonia;
using Avalonia.Layout;
namespace Avalonia.Layout
{
internal struct UvBounds
{
public UvBounds(Orientation orientation, Rect rect)
{
if (orientation == Orientation.Horizontal)
{
UMin = rect.Left;
UMax = rect.Right;
VMin = rect.Top;
VMax = rect.Bottom;
}
else
{
UMin = rect.Top;
UMax = rect.Bottom;
VMin = rect.Left;
VMax = rect.Right;
}
}
public double UMin { get; }
public double UMax { get; }
public double VMin { get; }
public double VMax { get; }
}
}

45
src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs

@ -1,45 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Avalonia.Layout;
namespace Avalonia.Layout
{
internal struct UvMeasure
{
internal static readonly UvMeasure Zero = default;
internal double U { get; set; }
internal double V { get; set; }
public UvMeasure(Orientation orientation, double width, double height)
{
if (orientation == Orientation.Horizontal)
{
U = width;
V = height;
}
else
{
U = height;
V = width;
}
}
public override bool Equals(object? obj)
{
if (obj is UvMeasure measure)
{
return (measure.U == U) && (measure.V == V);
}
return false;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
}

42
src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs

@ -1,42 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Layout
{
public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
{
private readonly VirtualizingLayoutContext _virtualizingContext;
private ChildrenCollection? _children;
public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
{
_virtualizingContext = virtualizingContext;
}
protected override object? LayoutStateCore
{
get => _virtualizingContext.LayoutState;
set => _virtualizingContext.LayoutState = value;
}
protected override IReadOnlyList<Layoutable> ChildrenCore =>
_children ?? (_children = new ChildrenCollection(_virtualizingContext));
private class ChildrenCollection : IReadOnlyList<Layoutable>
{
private readonly VirtualizingLayoutContext _context;
public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
public Layoutable this[int index] => _context.GetOrCreateElementAt(index);
public int Count => _context.ItemCount;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<Layoutable> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
}
}
}

119
src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs

@ -1,119 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Specialized;
namespace Avalonia.Layout
{
/// <summary>
/// Represents the base class for an object that sizes and arranges child elements for a host
/// and supports virtualization.
/// </summary>
/// <remarks>
/// <see cref="VirtualizingLayout"/> is the base class for layouts that support virtualization.
/// You can use one of the provided derived class, or inherit from it to create your own layout.
/// Provided concrete virtualizing layout classes are <see cref="StackLayout"/> and
/// <see cref="UniformGridLayout"/>.
/// </remarks>
public abstract class VirtualizingLayout : AttachedLayout
{
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="source">The data source.</param>
/// <param name="args">Data about the collection change.</param>
/// <remarks>
/// Override <see cref="OnItemsChangedCore(VirtualizingLayoutContext, object, NotifyCollectionChangedEventArgs)"/>
/// to provide the behavior for this method in a derived class.
/// </remarks>
public void OnItemsChanged(
VirtualizingLayoutContext context,
object? source,
NotifyCollectionChangedEventArgs args) => OnItemsChangedCore(context, source, args);
/// <summary>
/// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{
}
/// <summary>
/// When overridden in a derived class, removes any state the layout previously stored on
/// the ILayoutable container.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{
}
/// <summary>
/// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
/// this method to define their own "Measure" pass behavior.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="availableSize">
/// The available size that this object can give to child objects. Infinity can be
/// specified as a value to indicate that the object will size to whatever content is
/// available.
/// </param>
/// <returns>
/// The size that this object determines it needs during layout, based on its calculations
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
/// layout. Classes can override this method to define their own "Arrange" pass behavior.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="finalSize">
/// The final area within the container that this object should use to arrange itself and
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected internal virtual Size ArrangeOverride(
VirtualizingLayoutContext context,
Size finalSize) => finalSize;
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
/// </summary>
/// <param name="context">
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
/// <param name="source">The data source.</param>
/// <param name="args">Data about the collection change.</param>
protected internal virtual void OnItemsChangedCore(
VirtualizingLayoutContext context,
object? source,
NotifyCollectionChangedEventArgs args) => InvalidateMeasure();
}
}

195
src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs

@ -1,195 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Layout
{
/// <summary>
/// Defines constants that specify whether to suppress automatic recycling of the retrieved
/// element or force creation of a new element.
/// </summary>
/// <remarks>
/// When you call <see cref="VirtualizingLayoutContext.GetOrCreateElementAt(int, ElementRealizationOptions)"/>,
/// you can specify whether to suppress automatic recycling of the retrieved element or force
/// creation of a new element. Elements retrieved with automatic recycling suppressed
/// (SuppressAutoRecycle) are ignored by the automatic recycling logic that clears realized
/// elements that were not retrieved as part of the current layout pass. You must explicitly
/// recycle these elements by passing them to the RecycleElement method to avoid memory leaks.
/// </remarks>
[Flags]
public enum ElementRealizationOptions
{
/// <summary>
/// No option is specified.
/// </summary>
None = 0x0,
/// <summary>
/// Creation of a new element is forced.
/// </summary>
ForceCreate = 0x1,
/// <summary>
/// The element is ignored by the automatic recycling logic.
/// </summary>
SuppressAutoRecycle = 0x2,
};
/// <summary>
/// Represents the base class for layout context types that support virtualization.
/// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext
{
private NonVirtualizingLayoutContext? _contextAdapter;
/// <summary>
/// Gets the number of items in the data.
/// </summary>
/// <remarks>
/// This property gets the value returned by ItemCountCore, which must be implemented in
/// a derived class.
/// </remarks>
public int ItemCount => ItemCountCore();
/// <summary>
/// Gets or sets the origin point for the estimated content size.
/// </summary>
/// <remarks>
/// LayoutOrigin is used by virtualizing layouts that rely on estimations when determining
/// the size and position of content. It allows the layout to fix-up the estimated origin
/// of the content as it changes due to on-going estimation or potentially identifying the
/// actual size to use. For example, it’s possible that as a user is scrolling back to the
/// top of the content that the layout's estimates for the content size that it reports as
/// part of its MeasureOverride become increasingly accurate. If the predicted position of
/// the content does not already match the previously predicted position (for example, if
/// the size of the elements ends up being smaller than previously thought), then the
/// layout can indicate a new origin. The viewport provided to the layout on subsequent
/// passes will take into account the adjusted origin.
/// </remarks>
public Point LayoutOrigin { get => LayoutOriginCore; set => LayoutOriginCore = value; }
/// <summary>
/// Gets an area that represents the viewport and buffer that the layout should fill with
/// realized elements.
/// </summary>
public Rect RealizationRect => RealizationRectCore();
/// <summary>
/// Gets the recommended index from which to start the generation and layout of elements.
/// </summary>
/// <remarks>
/// The recommended index might be the result of programmatically realizing an element and
/// requesting that it be brought into view. Or, it may be that a user drags the scrollbar
/// thumb so quickly that the new viewport and the viewport and buffer previously given to
/// the layout do not intersect, so a new index is suggested as the anchor from which to
/// generate and layout other elements.
/// </remarks>
public int RecommendedAnchorIndex => RecommendedAnchorIndexCore;
/// <summary>
/// Implements the behavior of LayoutOrigin in a derived or custom VirtualizingLayoutContext.
/// </summary>
protected abstract Point LayoutOriginCore { get; set; }
/// <summary>
/// Implements the behavior for getting the return value of RecommendedAnchorIndex in a
/// derived or custom <see cref="VirtualizingLayoutContext"/>.
/// </summary>
protected virtual int RecommendedAnchorIndexCore { get; }
/// <summary>
/// Retrieves the data item in the source found at the specified index.
/// </summary>
/// <param name="index">The index of the data item to retrieve.</param>
public object GetItemAt(int index) => GetItemAtCore(index);
/// <summary>
/// Retrieves a UIElement that represents the data item in the source found at the
/// specified index. By default, if an element already exists, it is returned; otherwise,
/// a new element is created.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <remarks>
/// This method calls <see cref="GetOrCreateElementAtCore(int, ElementRealizationOptions)"/>
/// with options set to None. GetElementAtCore must be implemented in a derived class.
/// </remarks>
public Layoutable GetOrCreateElementAt(int index)
=> GetOrCreateElementAtCore(index, ElementRealizationOptions.None);
/// <summary>
/// Retrieves a UIElement that represents the data item in the source found at the
/// specified index using the specified options.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <param name="options">
/// A value of <see cref="ElementRealizationOptions"/> that specifies whether to suppress
/// automatic recycling of the retrieved element or force creation of a new element.
/// </param>
/// <remarks>
/// This method calls <see cref="GetOrCreateElementAtCore(int, ElementRealizationOptions)"/>,
/// which must be implemented in a derived class. When you request an element for the
/// specified index, you can optionally specify whether to suppress automatic recycling of
/// the retrieved element or force creation of a new element.Elements retrieved with
/// automatic recycling suppressed(SuppressAutoRecycle) are ignored by the automatic
/// recycling logic that clears realized elements that were not retrieved as part of the
/// current layout pass.You must explicitly recycle these elements by passing them to the
/// RecycleElement method to avoid memory leaks. These options are intended for more
/// advanced layouts that choose to explicitly manage the realization and recycling of
/// elements as a performance optimization.
/// </remarks>
public Layoutable GetOrCreateElementAt(int index, ElementRealizationOptions options)
=> GetOrCreateElementAtCore(index, options);
/// <summary>
/// Clears the specified UIElement and allows it to be either re-used or released.
/// </summary>
/// <param name="element">The element to clear.</param>
/// <remarks>
/// This method calls <see cref="RecycleElementCore(Layoutable)"/>, which must be implemented
/// in a derived class.
/// </remarks>
public void RecycleElement(Layoutable element) => RecycleElementCore(element);
/// <summary>
/// When implemented in a derived class, retrieves the number of items in the data.
/// </summary>
protected abstract int ItemCountCore();
/// <summary>
/// When implemented in a derived class, retrieves the data item in the source found at the
/// specified index.
/// </summary>
/// <param name="index">The index of the data item to retrieve.</param>
protected abstract object GetItemAtCore(int index);
/// <summary>
/// When implemented in a derived class, retrieves an area that represents the viewport and
/// buffer that the layout should fill with realized elements.
/// </summary>
protected abstract Rect RealizationRectCore();
/// <summary>
/// When implemented in a derived class, retrieves a UIElement that represents the data item
/// in the source found at the specified index using the specified options.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <param name="options">
/// A value of <see cref="ElementRealizationOptions"/> that specifies whether to suppress
/// automatic recycling of the retrieved element or force creation of a new element.
/// </param>
protected abstract Layoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options);
/// <summary>
/// When implemented in a derived class, clears the specified UIElement and allows it to be
/// either re-used or released.
/// </summary>
/// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(Layoutable element);
internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
}
}

25
src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs

@ -1,25 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Avalonia.Controls;
using Avalonia.Layout;
namespace Avalonia.Layout
{
internal class WrapItem
{
public WrapItem(int index)
{
this.Index = index;
}
public int Index { get; }
public UvMeasure? Measure { get; internal set; }
public UvMeasure? Position { get; internal set; }
public Layoutable? Element { get; internal set; }
}
}

336
src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs

@ -1,336 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Layout;
using System;
using System.Collections.Specialized;
namespace Avalonia.Layout
{
/// <summary>
/// Arranges elements by wrapping them to fit the available space.
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
/// </summary>
public class WrapLayout : VirtualizingLayout
{
/// <summary>
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
/// </summary>
public double HorizontalSpacing
{
get { return (double)GetValue(HorizontalSpacingProperty); }
set { SetValue(HorizontalSpacingProperty, value); }
}
/// <summary>
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> HorizontalSpacingProperty =
AvaloniaProperty.Register<WrapLayout, double>(nameof(HorizontalSpacing), 0);
/// <summary>
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
/// </summary>
public double VerticalSpacing
{
get { return (double)GetValue(VerticalSpacingProperty); }
set { SetValue(VerticalSpacingProperty, value); }
}
/// <summary>
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> VerticalSpacingProperty =
AvaloniaProperty.Register<WrapLayout, double>(
nameof(VerticalSpacing), 0d);
/// <summary>
/// Gets or sets the orientation of the WrapLayout.
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
/// </summary>
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
/// <summary>
/// Identifies the <see cref="Orientation"/> dependency property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<WrapLayout, Orientation>(
nameof(Orientation),
Orientation.Horizontal);
/// <inheritdoc />
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = new WrapLayoutState(context);
context.LayoutState = state;
base.InitializeForContextCore(context);
}
/// <inheritdoc />
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = null;
base.UninitializeForContextCore(context);
}
/// <inheritdoc />
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args)
{
var state = (WrapLayoutState)context.LayoutState!;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
state.RemoveFromIndex(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
state.RemoveFromIndex(minIndex);
state.RecycleElementAt(args.OldStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
state.RemoveFromIndex(args.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
state.RemoveFromIndex(args.NewStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
state.Clear();
break;
}
base.OnItemsChangedCore(context, source, args);
}
/// <inheritdoc />
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var totalMeasure = UvMeasure.Zero;
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
var position = UvMeasure.Zero;
var state = (WrapLayoutState)context.LayoutState!;
if (state.Orientation != Orientation)
{
state.SetOrientation(Orientation);
}
if (spacingMeasure.Equals(state.Spacing) == false)
{
state.ClearPositions();
state.Spacing = spacingMeasure;
}
if (state.AvailableU != parentMeasure.U)
{
state.ClearPositions();
state.AvailableU = parentMeasure.U;
}
double currentV = 0;
for (int i = 0; i < context.ItemCount; i++)
{
bool measured = false;
WrapItem item = state.GetItemAt(i);
if (item.Measure == null)
{
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
measured = true;
}
UvMeasure currentMeasure = item.Measure.Value;
if (currentMeasure.U == 0)
{
continue; // ignore collapsed items
}
if (item.Position == null)
{
if (parentMeasure.U < position.U + currentMeasure.U)
{
// New Row
position.U = 0;
position.V += currentV + spacingMeasure.V;
currentV = 0;
}
item.Position = position;
}
position = item.Position.Value;
double vEnd = position.V + currentMeasure.V;
if (vEnd < realizationBounds.VMin)
{
// Item is "above" the bounds
if (item.Element != null)
{
context.RecycleElement(item.Element);
item.Element = null;
}
}
else if (position.V > realizationBounds.VMax)
{
// Item is "below" the bounds.
if (item.Element != null)
{
context.RecycleElement(item.Element);
item.Element = null;
}
// We don't need to measure anything below the bounds
break;
}
else if (measured == false)
{
// Always measure elements that are within the bounds
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
if (currentMeasure.Equals(item.Measure) == false)
{
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Measure = currentMeasure;
// did the change make it go into the new row?
if (parentMeasure.U < position.U + currentMeasure.U)
{
// New Row
position.U = 0;
position.V += currentV + spacingMeasure.V;
currentV = 0;
}
item.Position = position;
}
}
position.U += currentMeasure.U + spacingMeasure.U;
currentV = Math.Max(currentMeasure.V, currentV);
}
// update value with the last line
// if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it
// if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here
// for the last condition it is zeros so adding it will make no difference
// this way is faster than an if condition in every loop for checking the last item
totalMeasure.U = parentMeasure.U;
// Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite
// axis to the panel. Clearing to zero prevents the crash.
// This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash.
if (double.IsInfinity(totalMeasure.U))
{
totalMeasure.U = 0.0;
}
totalMeasure.V = state.GetHeight();
totalMeasure.U = Math.Ceiling(totalMeasure.U);
return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U);
}
/// <inheritdoc />
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
if (context.ItemCount > 0)
{
var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height);
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
var state = (WrapLayoutState)context.LayoutState!;
bool Arrange(WrapItem item, bool isLast = false)
{
if (item.Measure.HasValue == false)
{
return false;
}
if (item.Position == null)
{
return false;
}
var desiredMeasure = item.Measure.Value;
if (desiredMeasure.U == 0)
{
return true; // if an item is collapsed, avoid adding the spacing
}
UvMeasure position = item.Position.Value;
// Stretch the last item to fill the available space
if (isLast)
{
desiredMeasure.U = parentMeasure.U - position.U;
}
if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax))
{
// place the item
var child = context.GetOrCreateElementAt(item.Index);
if (Orientation == Orientation.Horizontal)
{
child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V));
}
else
{
child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U));
}
}
else if (position.V > realizationBounds.VMax)
{
return false;
}
return true;
}
for (var i = 0; i < context.ItemCount; i++)
{
bool continueArranging = Arrange(state.GetItemAt(i));
if (continueArranging == false)
{
break;
}
}
}
return finalSize;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty)
{
InvalidateMeasure();
InvalidateArrange();
}
}
}
}

146
src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs

@ -1,146 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Layout
{
internal class WrapLayoutState
{
private List<WrapItem> _items = new List<WrapItem>();
private VirtualizingLayoutContext _context;
public WrapLayoutState(VirtualizingLayoutContext context)
{
this._context = context;
}
public Orientation Orientation { get; private set; }
public UvMeasure Spacing { get; internal set; }
public double AvailableU { get; internal set; }
internal WrapItem GetItemAt(int index)
{
if (index < 0)
{
throw new IndexOutOfRangeException();
}
if (index <= (_items.Count - 1))
{
return _items[index];
}
else
{
WrapItem item = new WrapItem(index);
_items.Add(item);
return item;
}
}
internal void Clear()
{
_items.Clear();
}
internal void RemoveFromIndex(int index)
{
if (index >= _items.Count)
{
// Item was added/removed but we haven't realized that far yet
return;
}
int numToRemove = _items.Count - index;
_items.RemoveRange(index, numToRemove);
}
internal void SetOrientation(Orientation orientation)
{
foreach (var item in _items)
{
if (!item.Measure.HasValue)
continue;
UvMeasure measure = item.Measure.Value;
double v = measure.V;
measure.V = measure.U;
measure.U = v;
item.Measure = measure;
item.Position = null;
}
Orientation = orientation;
AvailableU = 0;
}
internal void ClearPositions()
{
foreach (var item in _items)
{
item.Position = null;
}
}
internal double GetHeight()
{
if (_items.Count == 0)
{
return 0;
}
bool calculateAverage = true;
if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue)
{
calculateAverage = false;
}
UvMeasure? lastPosition = null;
double maxV = 0;
int itemCount = _items.Count;
for (int i = _items.Count - 1; i >= 0; i--)
{
var item = _items[i];
if (item.Position == null)
{
itemCount--;
continue;
}
if (lastPosition != null)
{
if (lastPosition.Value.V > item.Position.Value.V)
{
// This is a row above the last item. Exit and calculate the average
break;
}
}
lastPosition = item.Position;
maxV = Math.Max(maxV, item.Measure!.Value.V);
}
double totalHeight = lastPosition!.Value.V + maxV;
if (calculateAverage)
{
return (totalHeight / itemCount) * _context.ItemCount;
}
else
{
return totalHeight;
}
}
internal void RecycleElementAt(int index)
{
var element = _context.GetOrCreateElementAt(index);
_context.RecycleElement(element);
}
}
}

4
src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs

@ -1,4 +0,0 @@
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]

22
tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj

@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

24
tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs

@ -1,24 +0,0 @@
using System.Collections.ObjectModel;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ItemsRepeaterTests
{
[Fact]
public void Can_Reassign_Items()
{
var target = new ItemsRepeater();
target.ItemsSource = new ObservableCollection<string>();
target.ItemsSource = new ObservableCollection<string>();
}
[Fact]
public void Can_Reassign_Items_To_Null()
{
var target = new ItemsRepeater();
target.ItemsSource = new ObservableCollection<string>();
target.ItemsSource = null;
}
}
}

336
tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs

@ -1,336 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Base.UnitTests.Layout
{
public class NonVirtualizingStackLayoutTests
{
[Fact]
public void Lays_Out_Children_Vertically()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical };
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 100), desiredSize);
Assert.Equal(new Size(120, 100), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal };
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(100, 120), desiredSize);
Assert.Equal(new Size(100, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Vertically_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Arranges_Vertical_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical
};
var context = CreateContext(new[]
{
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(150, 10),
},
});
var desiredSize = target.Measure(context, new Size(100, 150));
Assert.Equal(new Size(100, 80), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 50, 10),
new Rect(0, 10, 100, 10),
new Rect(25, 20, 50, 10),
new Rect(0, 30, 100, 10),
new Rect(50, 40, 50, 10),
new Rect(0, 50, 100, 10),
new Rect(0, 60, 100, 10),
new Rect(0, 70, 100, 10),
}, bounds);
}
[Fact]
public void Arranges_Horizontal_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal
};
var context = CreateContext(new[]
{
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 150),
},
});
var desiredSize = target.Measure(context, new Size(150, 100));
Assert.Equal(new Size(80, 100), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 10, 50),
new Rect(10, 0, 10, 100),
new Rect(20, 25, 10, 50),
new Rect(30, 0, 10, 100),
new Rect(40, 50, 10, 50),
new Rect(50, 0, 10, 100),
new Rect(60, 0, 10, 100),
new Rect(70, 0, 10, 100),
}, bounds);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout
{
Orientation = orientation,
Spacing = 40,
};
var contextThreeChildrenOneInvisble = CreateContext(new[]
{
new StackPanel { Width = 10, Height= 10, IsVisible = false },
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 },
});
var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var contextTwoChildrenNoneInvisible = CreateContext(new[]
{
new StackPanel { Width = 10, Height = 10 },
new StackPanel { Width = 10, Height = 10 }
});
var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity);
var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity);
Assert.Equal(desiredSize2, desiredSize1);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Only_Arrange_Visible_Children(Orientation orientation)
{
var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
var panel = new Panel { Width = 10, Height = 10 };
var target = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var context = CreateContext(new[]
{
hiddenPanel,
panel
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(10, 10), arrangeSize);
}
private static NonVirtualizingLayoutContext CreateContext(Control[] children)
{
return new TestLayoutContext(children);
}
private class TestLayoutContext : NonVirtualizingLayoutContext
{
public TestLayoutContext(Control[] children) => ChildrenCore = children;
protected override IReadOnlyList<Layoutable> ChildrenCore { get; }
}
private class TestControl : Control
{
public Size MeasureConstraint { get; private set; }
public Size MeasureSize { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureConstraint = availableSize;
return MeasureSize;
}
}
}
}

1
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@ -11,7 +11,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

36
tests/Avalonia.LeakTests/ControlTests.cs

@ -714,40 +714,6 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void ItemsRepeater_Is_Freed()
{
using (Start())
{
Func<Window> run = () =>
{
var window = new Window
{
Content = new ItemsRepeater(),
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<ItemsRepeater>(window.Presenter.Child);
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ItemsRepeater>()).ObjectsCount));
}
}
[Fact]
public void ElementName_Binding_In_DataTemplate_Is_Freed()
{
@ -836,7 +802,7 @@ namespace Avalonia.LeakTests
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
var tl = new Window
{
Content = new ItemsRepeater(),
Content = new ItemsControl(),
};
tl.Show();

1
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -17,7 +17,6 @@
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>

52
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -16,11 +16,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class StyleTests : XamlTestBase
{
static StyleTests()
{
GC.KeepAlive(typeof(ItemsRepeater));
}
[Fact]
public void Color_Can_Be_Added_To_Style_Resources()
{
@ -399,53 +394,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater()
{
GC.KeepAlive(typeof(ItemsRepeater));
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Foreground' Value='Transparent'/>
</Style>
<Style Selector='TextBlock:nth-child(2n)'>
<Setter Property='Foreground' Value='{Binding}'/>
</Style>
</Window.Styles>
<ItemsRepeater x:Name='list' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var collection = new ObservableCollection<IBrush>()
{
Brushes.Red, Brushes.Green, Brushes.Blue
};
var list = window.FindControl<ItemsRepeater>("list");
list.ItemsSource = collection;
window.Show();
IEnumerable<IBrush> GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count)
.Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors().ToList());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
[Fact]
public void Style_Can_Use_Or_Selector_1()
{

1
tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs

@ -16,7 +16,6 @@ namespace Avalonia.Markup.Xaml.UnitTests
public static void Init()
{
var _ = typeof(Binding);
GC.KeepAlive(typeof(ItemsRepeater).Assembly);
}
}

Loading…
Cancel
Save