committed by
GitHub
54 changed files with 1 additions and 7798 deletions
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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;} |
|||
} |
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
}; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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) { } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,4 +0,0 @@ |
|||
using Avalonia.Metadata; |
|||
|
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] |
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue