Browse Source

Merge branch 'master' into managed-file-dialog

pull/2777/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
f00f2df06a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .editorconfig
  2. 1
      samples/ControlCatalog/MainView.xaml
  3. 25
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  4. 71
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  5. 3
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  6. 1
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  7. 31
      src/Avalonia.Base/AvaloniaObject.cs
  8. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  9. 1
      src/Avalonia.Controls/ContextMenu.cs
  10. 1
      src/Avalonia.Controls/GridSplitter.cs
  11. 9
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  12. 1
      src/Avalonia.Controls/Menu.cs
  13. 1
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  14. 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  15. 5
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  16. 1
      src/Avalonia.Controls/Primitives/TabStrip.cs
  17. 1
      src/Avalonia.Controls/Primitives/Track.cs
  18. 7
      src/Avalonia.Controls/ProgressBar.cs
  19. 54
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  20. 724
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  21. 24
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
  22. 44
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  23. 35
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
  24. 143
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  25. 106
      src/Avalonia.Controls/Repeater/RecyclePool.cs
  26. 65
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  27. 54
      src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
  28. 682
      src/Avalonia.Controls/Repeater/ViewManager.cs
  29. 501
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  30. 118
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  31. 15
      src/Avalonia.Controls/ScrollViewer.cs
  32. 1
      src/Avalonia.Controls/Slider.cs
  33. 4
      src/Avalonia.Controls/StackPanel.cs
  34. 1
      src/Avalonia.Controls/WrapPanel.cs
  35. 36
      src/Avalonia.Diagnostics/DevTools.xaml
  36. 33
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  37. 7
      src/Avalonia.Diagnostics/Models/EventChainLink.cs
  38. 2
      src/Avalonia.Diagnostics/ViewLocator.cs
  39. 12
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  40. 1
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  41. 15
      src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
  42. 9
      src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
  43. 17
      src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
  44. 12
      src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
  45. 3
      src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
  46. 16
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  47. 15
      src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
  48. 22
      src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
  49. 7
      src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
  50. 5
      src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
  51. 17
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  52. 100
      src/Avalonia.Diagnostics/Views/EventsView.xaml
  53. 8
      src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
  54. 3
      src/Avalonia.Diagnostics/Views/GridRepeater.cs
  55. 10
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  56. 8
      src/Avalonia.Diagnostics/Views/SimpleGrid.cs
  57. 12
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  58. 4
      src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
  59. 5
      src/Avalonia.Diagnostics/VisualTreeDebug.cs
  60. 106
      src/Avalonia.Layout/AttachedLayout.cs
  61. 460
      src/Avalonia.Layout/ElementManager.cs
  62. 712
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  63. 44
      src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs
  64. 24
      src/Avalonia.Layout/LayoutContext.cs
  65. 103
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  66. 2
      src/Avalonia.Layout/Orientation.cs
  67. 96
      src/Avalonia.Layout/OrientationBasedMeasures.cs
  68. 336
      src/Avalonia.Layout/StackLayout.cs
  69. 61
      src/Avalonia.Layout/StackLayoutState.cs
  70. 520
      src/Avalonia.Layout/UniformGridLayout.cs
  71. 192
      src/Avalonia.Layout/UniformGridLayoutState.cs
  72. 32
      src/Avalonia.Layout/Utils/ListUtils.cs
  73. 139
      src/Avalonia.Layout/VirtualizingLayout.cs
  74. 190
      src/Avalonia.Layout/VirtualizingLayoutContext.cs
  75. 1
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  76. 4
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  77. 1
      tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
  78. 1
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  79. 33
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  80. 1
      tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs
  81. 1
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  82. 1
      tests/Avalonia.Controls.UnitTests/SliderTests.cs
  83. 3
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
  84. 1
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
  85. 1
      tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs

3
.editorconfig

@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
space_within_single_line_array_initializer_braces = true
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
indent_size = 4
indent_size = 2
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]

1
samples/ControlCatalog/MainView.xaml

@ -26,6 +26,7 @@
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="ItemsRepeater"><pages:ItemsRepeaterPage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>

25
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
<TextBlock Classes="h1">ItemsRepeater</TextBlock>
<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">
<ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
<ComboBoxItem>Stack - Vertical</ComboBoxItem>
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
</ComboBox>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Items="{Binding}"/>
</ScrollViewer>
</Border>
</DockPanel>
</UserControl>

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

@ -0,0 +1,71 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
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;
}
}
}
}

3
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,11 +1,10 @@
using System.Reactive;
using Avalonia.Controls.Notifications;
using Avalonia.Diagnostics.ViewModels;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
class MainWindowViewModel : ViewModelBase
class MainWindowViewModel : ReactiveObject
{
private IManagedNotificationManager _notificationManager;

1
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI.Legacy;
using ReactiveUI;
using Avalonia.Layout;
namespace VirtualizationDemo.ViewModels
{

31
src/Avalonia.Base/AvaloniaObject.cs

@ -163,6 +163,37 @@ namespace Avalonia
SetValue(property, AvaloniaProperty.UnsetValue);
}
/// <summary>
/// Compares two objects using reference equality.
/// </summary>
/// <param name="obj">The object to compare.</param>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override bool Equals(object obj) => base.Equals(obj);
/// <summary>
/// Gets the hash code for the object.
/// </summary>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>

1
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -24,6 +24,7 @@ using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
namespace Avalonia.Controls
{

1
src/Avalonia.Controls/ContextMenu.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls

1
src/Avalonia.Controls/GridSplitter.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls

9
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -0,0 +1,9 @@
namespace Avalonia.Controls
{
public interface IScrollAnchorProvider
{
IControl CurrentAnchor { get; }
void RegisterAnchorCandidate(IControl element);
void UnregisterAnchorCandidate(IControl element);
}
}

1
src/Avalonia.Controls/Menu.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{

1
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{

1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{

5
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -333,6 +333,11 @@ namespace Avalonia.Controls.Primitives
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
}
break;
}
}

1
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -4,6 +4,7 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{

1
src/Avalonia.Controls/Primitives/Track.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Metadata;
namespace Avalonia.Controls.Primitives

7
src/Avalonia.Controls/ProgressBar.cs

@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -33,8 +34,8 @@ namespace Avalonia.Controls
static ProgressBar()
{
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
@ -120,4 +121,4 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
}
}
}

54
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

@ -0,0 +1,54 @@
// 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
{
private readonly IDataTemplate _dataTemplate;
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
public IControl GetElement(IControl parent, object data)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
IControl 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;
}
public void RecycleElement(IControl parent, IControl 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);
}
}
}

724
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -0,0 +1,724 @@
// 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;
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
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<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="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.
/// </summary>
public static readonly AvaloniaProperty<AttachedLayout> LayoutProperty =
AvaloniaProperty.Register<ItemsRepeater, AttachedLayout>(nameof(Layout), new StackLayout());
/// <summary>
/// Defines the <see cref="VerticalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> VerticalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0);
private static readonly AttachedProperty<VirtualizationInfo> VirtualizationInfoProperty =
AvaloniaProperty.RegisterAttached<ItemsRepeater, IControl, 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 IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private bool _isLayoutInProgress;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsRepeater"/> class.
/// </summary>
public ItemsRepeater()
{
_viewManager = new ViewManager(this);
_viewportManager = new ViewportManager(this);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
OnLayoutChanged(null, Layout);
}
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
}
/// <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 Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
/// <summary>
/// Gets or sets the template used to display each item.
/// </summary>
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 ItemTemplateWrapper ItemTemplateShim { get; set; }
internal Point LayoutOrigin { get; set; }
internal object LayoutState { get; set; }
internal IControl MadeAnchor => _viewportManager.MadeAnchor;
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
private LayoutContext LayoutContext
{
get
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
/// <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="IControl"/> 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 IControl 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="IControl"/>.
/// </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(IControl 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 IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
{
var value = element.GetValue(VirtualizationInfoProperty);
return value;
}
internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
{
if (TryGetVirtualizationInfo(element) != null)
{
throw new InvalidOperationException("VirtualizationInfo already created.");
}
var result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
return result;
}
internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
{
var result = element.GetValue(VirtualizationInfoProperty);
if (result == null)
{
result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
}
return result;
}
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 = GetLayoutContext();
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)
{
ClearElementImpl(element);
}
}
}
_viewportManager.SetLayoutExtent(extent);
return desiredSize;
}
finally
{
_isLayoutInProgress = false;
}
}
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(GetLayoutContext(), 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.OnOwnerArranged();
return arrangeSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
var property = args.Property;
if (property == ItemsProperty)
{
var newValue = (IEnumerable)args.NewValue;
var newDataSource = newValue as ItemsSourceView;
if (newValue != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newValue);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
else if (property == ItemTemplateProperty)
{
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
}
else if (property == LayoutProperty)
{
OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
}
else if (property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = (double)args.NewValue;
}
else if (property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = (double)args.NewValue;
}
else
{
base.OnPropertyChanged(args);
}
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
{
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
return element;
}
internal void ClearElementImpl(IControl 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);
}
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
private IControl GetElementFromIndexImpl(int index)
{
IControl 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 IControl GetOrCreateElementImpl(int index)
{
if (index >= 0 && index >= ItemsSourceView.Count)
{
throw new ArgumentException("Argument index is invalid.", "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 = (IControl)GetLayoutContext().GetOrCreateElementAt(index);
element.Measure(Size.Infinity);
}
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
InvalidateMeasure();
return element;
}
internal void OnElementPrepared(IControl element, int index)
{
_viewportManager.OnElementPrepared(element);
if (ElementPrepared != null)
{
if (_elementPreparedArgs == null)
{
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
}
else
{
_elementPreparedArgs.Update(element, index);
}
ElementPrepared(this, _elementPreparedArgs);
}
}
internal void OnElementClearing(IControl element)
{
if (ElementClearing != null)
{
if (_elementClearingArgs == null)
{
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
}
else
{
_elementClearingArgs.Update(element);
}
ElementClearing(this, _elementClearingArgs);
}
}
internal void OnElementIndexChanged(IControl 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);
}
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;
}
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
// 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);
}
}
}
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(GetLayoutContext(), 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 = 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);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
// 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);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
}
bool isVirtualizingLayout = newValue != null && 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(GetLayoutContext(), sender, args);
}
else
{
// NonVirtualizingLayout
InvalidateMeasure();
}
}
}
finally
{
_processingItemsSourceChange = null;
}
}
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
private VirtualizingLayoutContext GetLayoutContext()
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
}

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

@ -0,0 +1,24 @@
// 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(IControl element) => Element = element;
/// <summary>
/// Gets the element that is being cleared for re-use.
/// </summary>
public IControl Element { get; private set; }
internal void Update(IControl element) => Element = element;
}
}

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

@ -0,0 +1,44 @@
// 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(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
/// <summary>
/// Get the element for which the index changed.
/// </summary>
public IControl 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(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
}

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

@ -0,0 +1,35 @@
// 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(IControl element, int index)
{
Element = element;
Index = index;
}
/// <summary>
/// Gets the prepared element.
/// </summary>
public IControl 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(IControl element, int index)
{
Element = element;
Index = index;
}
}
}

143
src/Avalonia.Controls/Repeater/ItemsSourceView.cs

@ -0,0 +1,143 @@
// 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.Collections.Specialized;
using System.Linq;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> control.
/// </summary>
/// <remarks>
/// Components written to work with ItemsRepeater should consume the
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
/// view of the Items. That way, each component does not need to know if the source is an
/// IEnumerable, an IList, or something else.
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
private int _cachedSize = -1;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
{
Contract.Requires<ArgumentNullException>(source != null);
_inner = source as IList;
if (_inner == null && source is IEnumerable<object> objectEnumerable)
{
_inner = new List<object>(objectEnumerable);
}
else
{
_inner = new List<object>(source.Cast<object>());
}
ListenToCollectionChanges();
}
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count
{
get
{
if (_cachedSize == -1)
{
_cachedSize = _inner.Count;
}
return _cachedSize;
}
}
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc/>
public void Dispose()
{
if (_notifyCollectionChanged != null)
{
_notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
}
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public string KeyFromIndex(int index)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the unique identifier (key) for the item at the specified index.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The index.</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public int IndexFromKey(string key)
{
throw new NotImplementedException();
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
_cachedSize = _inner.Count;
CollectionChanged?.Invoke(this, args);
}
private void ListenToCollectionChanges()
{
if (_inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
_notifyCollectionChanged = incc;
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
}
}
}

106
src/Avalonia.Controls/Repeater/RecyclePool.cs

@ -0,0 +1,106 @@
// 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
{
internal class RecyclePool
{
public static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
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(IControl element, string key, IControl 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 IControl TryGetElement(string key, IControl 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;
}
private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
{
if (owner is IPanel panel)
{
return panel;
}
else if (owner != null)
{
throw new InvalidOperationException("Owner must be IPanel or null.");
}
return null;
}
private class ElementInfo
{
public ElementInfo(IControl element, IPanel owner)
{
Element = element;
Owner = owner;
}
public IControl Element { get; }
public IPanel Owner { get;}
}
}
}

65
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -0,0 +1,65 @@
// 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;
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 ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
{
return _owner.GetElementImpl(
index,
(options & ElementRealizationOptions.ForceCreate) != 0,
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
}
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}
}

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

@ -0,0 +1,54 @@
// 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, IControl>>
{
private readonly Dictionary<string, IControl> _elementMap = new Dictionary<string, IControl>();
private readonly ItemsRepeater _owner;
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
public void Add(IControl 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 IControl 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, IControl>> GetEnumerator() => _elementMap.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

682
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -0,0 +1,682 @@
// 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 System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
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 IControl _lastFocusedElement;
private bool _isDataSourceStableResetPending;
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
private bool _eventsSubscribed;
public ViewManager(ItemsRepeater owner)
{
_owner = owner;
_resetPool = new UniqueIdElementPool(owner);
}
public IControl 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;
}
else
{
virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
}
return element;
}
public void ClearElement(IControl 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.
}
}
public void ClearElementToElementFactory(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element);
_owner.ItemTemplateShim.RecycleElement(_owner, element);
virtInfo.MoveOwnershipToElementFactory();
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(clearedIndex);
}
}
private void MoveFocusFromClearedIndex(int clearedIndex)
{
IControl focusedChild = null;
var focusCandidate = FindFocusCandidate(clearedIndex, 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 candiate.
_lastFocusedElement = null;
}
}
IControl FindFocusCandidate(int clearedIndex, IControl 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;
IControl nextElement = null;
IControl 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.
return nextElement;
}
public int GetElementIndex(VirtualizationInfo virtInfo)
{
if (virtInfo == null)
{
throw new ArgumentException("Element is not a child of this ItemsRepeater.");
}
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(IControl element, bool addPin)
{
var parent = element.VisualParent;
var child = (IVisual)element;
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)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.VisualParent;
}
}
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 (_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();
}
}
// 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 IControl GetElementIfAlreadyHeldByLayout(int index)
{
IControl 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 IControl GetElementFromUniqueIdResetPool(int index)
{
IControl 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);
}
}
return element;
}
private IControl GetElementFromPinnedElements(int index)
{
IControl 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();
break;
}
}
return element;
}
private IControl GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
var itemTemplateFactory = _owner.ItemTemplateShim;
if (itemTemplateFactory == null)
{
// If no ItemTemplate was provided, use a default
var factory = FuncDataTemplate.Default;
_owner.ItemTemplate = factory;
itemTemplateFactory = _owner.ItemTemplateShim;
}
var data = _owner.ItemsSourceView.GetAt(index);
var element = itemTemplateFactory.GetElement(_owner, data);
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null)
{
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
// Prepare the element
element.DataContext = data;
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.VisualParent != repeater)
{
children.Add(element);
}
repeater.OnElementPrepared(element, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
return element;
}
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private void UpdateFocusedElement()
{
IControl focusedElement = null;
var child = FocusManager.Instance.Current;
if (child != null)
{
var parent = child.VisualParent;
var owner = _owner;
// Find out if the focused element belongs to one of our direct
// children.
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var element = child as IControl;
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
{
focusedElement = element;
}
break;
}
child = parent as IInputElement;
parent = child.VisualParent;
}
}
// 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;
}
}
private void UpdateElementIndex(IControl 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(IControl element)
{
PinnedElement = element;
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
}
public IControl PinnedElement { get; }
public VirtualizationInfo VirtualizationInfo { get; }
}
}
}

501
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -0,0 +1,501 @@
// 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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Layout;
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 IControl _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Task _cacheBuildAction;
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 = 0.0;
private double _maximumVerticalCacheLength = 0.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.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
{
_owner = owner;
}
public IControl 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.VisualParent as IControl;
while (parent != null)
{
if (parent == owner)
{
suggestedAnchor = child;
break;
}
child = parent;
parent = parent.VisualParent as IControl;
}
}
}
return suggestedAnchor;
}
}
public bool HasScroller => _scroller != null;
public IControl 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;
}
}
}
private Rect GetLayoutVisibleWindowDiscardAnchor()
{
var visibleWindow = _visibleWindow;
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 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 invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
// 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.
((IControl)_scroller)?.InvalidateArrange();
}
public Point GetOrigin() => _layoutExtent.TopLeft;
public void OnLayoutChanged(bool isVirtualizing)
{
_managingViewportDisabled = !isVirtualizing;
_layoutExtent = default;
_expectedViewportShift = default;
_pendingViewportShift = default;
_unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
}
public void OnElementPrepared(IControl element)
{
// If we have an anchor element, we do not want the
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
}
public void OnElementCleared(ILayoutable element)
{
////element.CanBeScrollAnchor(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;
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)
{
// 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(IControl anchor, bool isAnchorOutsideRealizedRange)
{
_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((IControl)args.TargetObject);
// Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children)
{
////if (child.CanBeScrollAnchor && child != targetChild)
////{
//// child.CanBeScrollAnchor = false;
////}
}
// Register to rendering event to go back to how things were before where any child can be the anchor.
_isBringIntoViewInProgress = true;
////if (!m_renderingToken)
////{
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
////}
}
}
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
var parent = descendant.Parent;
while (parent != null && parent != _owner)
{
targetChild = parent;
parent = (IControl)parent.VisualParent;
}
if (parent == null)
{
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
}
return targetChild;
}
public void ResetScrollers()
{
_scroller = null;
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = null;
_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(TransformedBounds? bounds)
{
if (!bounds.HasValue)
{
return;
}
var globalClip = bounds.Value.Clip;
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
var clip = globalClip.TransformToAABB(transform);
var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
UpdateViewport(effectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.IsEmpty)
{
// 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;
}
}
private void EnsureScroller()
{
if (!_ensuredScroller)
{
ResetScrollers();
var parent = _owner.GetVisualParent();
while (parent != null)
{
if (parent is IScrollAnchorProvider scroller)
{
_scroller = scroller;
break;
}
parent = parent.VisualParent;
}
if (_scroller == null)
{
// We usually update the viewport in the post arrange handler. But, since we don't have
// a scroller, let's do it now.
UpdateViewport(Rect.Empty);
}
else if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
_ensuredScroller = true;
}
}
private void UpdateViewport(Rect viewport)
{
var currentVisibleWindow = viewport;
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
// We got cleared.
_visibleWindow = default;
}
else
{
_visibleWindow = 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.IsEmpty)
{
// 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.
_owner.InvalidateMeasure();
}
}
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, resulting in the UI being updated too late when scrolling quickly. We can
// partially remedey this by triggering also on Bounds changes, but this won't work so
// well for nested ItemsRepeaters.
//
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia.
return control.GetObservable(Visual.TransformedBoundsProperty)
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
.Skip(1)
.Subscribe(OnEffectiveViewportChanged);
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)
{
Scroller = scroller;
}
public ScrollViewer Scroller { get; }
}
};
}

118
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -0,0 +1,118 @@
// 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;
private object _data;
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 KeepAlive { 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;
}
}

15
src/Avalonia.Controls/ScrollViewer.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl, IScrollable
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -333,6 +333,9 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Gets the value of the HorizontalScrollBarVisibility attached property.
/// </summary>
@ -373,6 +376,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);

1
src/Avalonia.Controls/Slider.cs

@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{

4
src/Avalonia.Controls/StackPanel.cs

@ -17,13 +17,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
StackLayout.SpacingProperty.AddOwner<StackPanel>();
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<StackPanel, Orientation>(nameof(Orientation), Orientation.Vertical);
StackLayout.OrientationProperty.AddOwner<StackPanel>();
/// <summary>
/// Initializes static members of the <see cref="StackPanel"/> class.

1
src/Avalonia.Controls/WrapPanel.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;

36
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,24 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="*,Auto" Margin="4">
<Grid RowDefinitions="*,Auto" Margin="4">
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

33
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
@ -18,22 +17,22 @@ using Avalonia.VisualTree;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Avalonia.Diagnostics.DevTools.Attach(control);
}
}
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Diagnostics.DevTools.Attach(control);
}
}
}
namespace Avalonia.Diagnostics
{
public class DevTools : UserControl
public class DevTools : UserControl
{
private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private IDisposable _keySubscription;
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
@ -49,7 +48,6 @@ namespace Avalonia.Diagnostics
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
}
public IControl Root { get; }
@ -67,9 +65,8 @@ namespace Avalonia.Diagnostics
if (e.Key == Key.F12)
{
var control = (TopLevel)sender;
var devToolsWindow = default(Window);
if (s_open.TryGetValue(control, out devToolsWindow))
if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
@ -82,10 +79,7 @@ namespace Avalonia.Diagnostics
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates =
{
new ViewLocator<ViewModelBase>(),
},
DataTemplates = { new ViewLocator<ViewModelBase>() },
Title = "Avalonia DevTools"
};
@ -118,7 +112,6 @@ namespace Avalonia.Diagnostics
if ((e.Modifiers) == modifiers)
{
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();

7
src/Avalonia.Diagnostics/Models/EventChainLink.cs

@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models
{
Contract.Requires<ArgumentNullException>(handler != null);
this.Handler = handler;
this.Handled = handled;
this.Route = route;
Handler = handler;
Handled = handled;
Route = route;
}
public object Handler { get; }
@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models
{
return named.Name + " (" + Handler.GetType().Name + ")";
}
return Handler.GetType().Name;
}
}

2
src/Avalonia.Diagnostics/ViewLocator.cs

@ -7,7 +7,7 @@ using Avalonia.Controls.Templates;
namespace Avalonia.Diagnostics
{
public class ViewLocator<TViewModel> : IDataTemplate
internal class ViewLocator<TViewModel> : IDataTemplate
{
public bool SupportsRecycling => false;

12
src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public IEnumerable<string> Classes
{
get;
private set;
}
public IEnumerable<PropertyDetails> Properties
{
get;
private set;
}
public IEnumerable<PropertyDetails> Properties { get; }
}
}

1
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;

15
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[]
private static readonly RoutedEvent[] s_defaultEvents =
{
Button.ClickEvent,
InputElement.KeyDownEvent,
InputElement.KeyUpEvent,
InputElement.TextInputEvent,
InputElement.PointerReleasedEvent,
InputElement.PointerPressedEvent,
Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm)
: base(null, type.Name)
{
this.Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
this.IsExpanded = true;
IsExpanded = true;
}
public override bool? IsEnabled
@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (base.IsEnabled != value)
{
base.IsEnabled = value;
if (_updateChildren && value != null)
{
foreach (var child in Children)

9
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
using Avalonia.Threading;
@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
private RoutedEvent _event;
private EventsViewModel _parentViewModel;
private readonly RoutedEvent _event;
private readonly EventsViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires<ArgumentNullException>(@event != null);
Contract.Requires<ArgumentNullException>(vm != null);
this._event = @event;
this._parentViewModel = vm;
_event = @event;
_parentViewModel = vm;
}
public override bool? IsEnabled

17
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs

@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _isExpanded;
private bool? _isEnabled = false;
public EventTreeNodeBase(EventTreeNodeBase parent, string text)
protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
this.Parent = parent;
this.Text = text;
Parent = parent;
Text = text;
}
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public virtual bool? IsEnabled
{
get { return _isEnabled; }
set { RaiseAndSetIfChanged(ref _isEnabled, value); }
get => _isEnabled;
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
public EventTreeNodeBase Parent
@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Text
{
get;
private set;
}
internal void UpdateChecked()
@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (Children == null)
return false;
bool? value = false;
for (int i = 0; i < Children.Count; i++)
{
if (i == 0)

12
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs

@ -3,7 +3,6 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class FiredEvent : ViewModelBase
{
private RoutedEventArgs _eventArgs;
private readonly RoutedEventArgs _eventArgs;
private EventChainLink _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires<ArgumentNullException>(eventArgs != null);
Contract.Requires<ArgumentNullException>(originator != null);
this._eventArgs = eventArgs;
this.Originator = originator;
_eventArgs = eventArgs;
Originator = originator;
AddToChain(originator);
}
@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
}
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
}
}
@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink HandledBy
{
get { return _handledBy; }
get => _handledBy;
set
{
if (_handledBy != value)

3
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs

@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control)
{
var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
}
}
}

16
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels
Value = diagnostic.Value ?? "(null)";
Priority = (diagnostic.Priority != BindingPriority.Unset) ?
diagnostic.Priority.ToString() :
diagnostic.Property.Inherits ? "Inherited" : "Unset";
diagnostic.Property.Inherits ?
"Inherited" :
"Unset";
Diagnostic = diagnostic.Diagnostic;
});
}
@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels
public string Priority
{
get { return _priority; }
private set { RaiseAndSetIfChanged(ref _priority, value); }
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public string Diagnostic
{
get { return _diagnostic; }
private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
get => _diagnostic;
private set => RaiseAndSetIfChanged(ref _diagnostic, value);
}
public object Value
{
get { return _value; }
private set { RaiseAndSetIfChanged(ref _value, value); }
get => _value;
private set => RaiseAndSetIfChanged(ref _value, value);
}
}
}

15
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs

@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(((IStyleable)styleable).StyleDetach);
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(styleable.StyleDetach);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes
{
get { return _classes; }
private set { RaiseAndSetIfChanged(ref _classes, value); }
get => _classes;
private set => RaiseAndSetIfChanged(ref _classes, value);
}
public IVisual Visual
@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public TreeNode Parent
@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type
{
get;
private set;
}
}
}

22
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs

@ -23,7 +23,7 @@ namespace Avalonia.Diagnostics.ViewModels
public TreeNode SelectedNode
{
get { return _selected; }
get => _selected;
set
{
if (RaiseAndSetIfChanged(ref _selected, value))
@ -35,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details
{
get { return _details; }
private set { RaiseAndSetIfChanged(ref _details, value); }
get => _details;
private set => RaiseAndSetIfChanged(ref _details, value);
}
public TreeNode FindNode(IControl control)
@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
control = control.GetVisualParent<IControl>();
}
}
}
if (node != null)
{
@ -90,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
return node;
}
else
foreach (var child in node.Children)
{
foreach (var child in node.Children)
{
var result = FindNode(child, control);
var result = FindNode(child, control);
if (result != null)
{
return result;
}
if (result != null)
{
return result;
}
}

7
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs

@ -1,11 +1,14 @@
using System.Collections.Generic;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
internal class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

5
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs

@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public bool IsInTemplate { get; private set; }
public bool IsInTemplate { get; }
public static VisualTreeNode[] Create(object control)
{
var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
}
}
}

17
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.Views
{
private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>(nameof(ViewModel));
private SimpleGrid _grid;
public ControlDetailsView()
@ -25,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
public ControlDetailsViewModel ViewModel
{
get { return GetValue(ViewModelProperty); }
get => GetValue(ViewModelProperty);
private set
{
SetValue(ViewModelProperty, value);
@ -37,13 +38,7 @@ namespace Avalonia.Diagnostics.Views
{
Func<object, IEnumerable<Control>> pt = PropertyTemplate;
Content = new ScrollViewer
{
Content = _grid = new SimpleGrid
{
[GridRepeater.TemplateProperty] = pt,
}
};
Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
}
private IEnumerable<Control> PropertyTemplate(object i)
@ -57,7 +52,7 @@ namespace Avalonia.Diagnostics.Views
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding()
};
yield return new TextBlock
@ -66,14 +61,14 @@ namespace Avalonia.Diagnostics.Views
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
.Select(v => v?.ToString())
.ToBinding(),
.ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding()
};
}
}

100
src/Avalonia.Diagnostics/Views/EventsView.xaml

@ -2,53 +2,57 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView">
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}" Text="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
</StackPanel>
</Grid>
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}"
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}"
Text="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

8
src/Avalonia.Diagnostics/Views/EventsView.xaml.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Markup.Xaml;
@ -11,15 +10,16 @@ namespace Avalonia.Diagnostics.Views
{
public class EventsView : UserControl
{
private ListBox _events;
private readonly ListBox _events;
public EventsView()
{
this.InitializeComponent();
InitializeComponent();
_events = this.FindControl<ListBox>("events");
}
private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
private void RecordedEvents_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_events.ScrollIntoView(_events.Items.OfType<FiredEvent>().LastOrDefault());
}

3
src/Avalonia.Diagnostics/Views/GridRepeater.cs

@ -14,7 +14,8 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterAttached<SimpleGrid, IEnumerable>("Items", typeof(GridRepeater));
public static readonly AttachedProperty<Func<object, IEnumerable<Control>>> TemplateProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, Func<object, IEnumerable<Control>>>("Template", typeof(GridRepeater));
AvaloniaProperty.RegisterAttached<SimpleGrid, Func<object, IEnumerable<Control>>>("Template",
typeof(GridRepeater));
static GridRepeater()
{

10
src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs

@ -23,11 +23,11 @@ namespace Avalonia.Diagnostics.Views
}
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
e => source.PropertyChanged += e,
e => source.PropertyChanged -= e)
.Where(e => e.EventArgs.PropertyName == propertyName)
.Select(_ => (T)property.GetValue(source))
.StartWith((T)property.GetValue(source));
e => source.PropertyChanged += e,
e => source.PropertyChanged -= e)
.Where(e => e.EventArgs.PropertyName == propertyName)
.Select(_ => (T)property.GetValue(source))
.StartWith((T)property.GetValue(source));
}
}
}

8
src/Avalonia.Diagnostics/Views/SimpleGrid.cs

@ -15,8 +15,8 @@ namespace Avalonia.Diagnostics.Views
/// </remarks>
public class SimpleGrid : Panel
{
private List<double> _columnWidths = new List<double>();
private List<double> _rowHeights = new List<double>();
private readonly List<double> _columnWidths = new List<double>();
private readonly List<double> _rowHeights = new List<double>();
private double _totalWidth;
private double _totalHeight;
@ -31,7 +31,7 @@ namespace Avalonia.Diagnostics.Views
/// </summary>
public static readonly AttachedProperty<int> RowProperty =
AvaloniaProperty.RegisterAttached<SimpleGrid, Control, int>("Row");
/// <summary>
/// Gets the value of the Column attached property for a control.
/// </summary>
@ -62,7 +62,7 @@ namespace Avalonia.Diagnostics.Views
control.SetValue(ColumnProperty, value);
}
/// <summary>
/// Sets the value of the Row attached property for a control.
/// </summary>

12
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@ -2,25 +2,25 @@
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
<Grid ColumnDefinitions="*,4,3*">
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Classes}"/>
<TextBlock Text="{Binding Type}" />
<TextBlock Text="{Binding Classes}" />
</StackPanel>
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1"/>
<ContentControl Content="{Binding Details}" Grid.Column="2"/>
<GridSplitter Width="4" Grid.Column="1" />
<ContentControl Content="{Binding Details}" Grid.Column="2" />
</Grid>
</UserControl>

4
src/Avalonia.Diagnostics/Views/TreePage.xaml.cs → src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
public TreePageView()
{
this.InitializeComponent();
InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
}
@ -39,7 +39,7 @@ namespace Avalonia.Diagnostics.Views
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Visual,
[AdornerLayer.AdornedElementProperty] = node.Visual
};
layer.Children.Add(_adorner);

5
src/Avalonia.Diagnostics/Debug.cs → src/Avalonia.Diagnostics/VisualTreeDebug.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Data;
@ -10,7 +9,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
public static class Debug
public static class VisualTreeDebug
{
public static string PrintVisualTree(IVisual visual)
{
@ -67,7 +66,7 @@ namespace Avalonia.Diagnostics
private static string Indent(int indent)
{
return string.Join(string.Empty, Enumerable.Repeat(" ", Math.Max(indent, 0)));
return new string(' ' , Math.Max(indent, 0) * 4);
}
}
}

106
src/Avalonia.Layout/AttachedLayout.cs

@ -0,0 +1,106 @@
// 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>
/// Represents the base class for an object that sizes and arranges child elements for a host.
/// </summary>
public abstract class AttachedLayout : AvaloniaObject
{
internal string LayoutId { get; set; }
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public event EventHandler MeasureInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public event EventHandler ArrangeInvalidated;
/// <summary>
/// Initializes any per-container state the layout requires when it is attached to an
/// <see cref="ILayoutable"/> 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 abstract void InitializeForContext(LayoutContext context);
/// <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 abstract void UninitializeForContext(LayoutContext context);
/// <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 abstract Size Measure(LayoutContext context, Size availableSize);
/// <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 abstract Size Arrange(LayoutContext context, Size finalSize);
/// <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);
}
}

460
src/Avalonia.Layout/ElementManager.cs

@ -0,0 +1,460 @@
// 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;
namespace Avalonia.Layout
{
internal class ElementManager
{
private readonly List<ILayoutable> _realizedElements = new List<ILayoutable>();
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 realizaton
// 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);
}
}
}
}
public int GetRealizedElementCount()
{
return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount;
}
public ILayoutable GetAt(int realizedIndex)
{
ILayoutable element;
if (IsVirtualizingContext)
{
if (_realizedElements[realizedIndex] == null)
{
// Sentinel. Create the element now since we need it.
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
element = _context.GetOrCreateElementAt(
dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
_realizedElements[realizedIndex] = element;
}
else
{
element = _realizedElements[realizedIndex];
}
}
else
{
// realizedIndex and dataIndex are the same (everything is realized)
element = _context.GetOrCreateElementAt(
realizedIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
}
return element;
}
public void Add(ILayoutable element, int dataIndex)
{
if (_realizedElements.Count == 0)
{
_firstRealizedDataIndex = dataIndex;
}
_realizedElements.Add(element);
_realizedElementLayoutBounds.Add(default);
}
public void Insert(int realizedIndex, int dataIndex, ILayoutable 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) => currentIndex >= 0 && currentIndex < _context.ItemCount;
public ILayoutable 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);
}
}
}
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;
case NotifyCollectionChangedAction.Remove:
{
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
}
break;
case NotifyCollectionChangedAction.Reset:
ClearRealizedRange();
break;
case NotifyCollectionChangedAction.Move:
throw new NotImplementedException();
}
}
}
public int GetElementDataIndex(ILayoutable 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;
}
}
}
}

712
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@ -0,0 +1,712 @@
// 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
{
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 folow 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,
ScrollOrientation orientation,
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;
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, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
if (isWrapping && IsReflowRequired())
{
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
}
RaiseLineArranged();
_collectionChangePending = false;
_lastExtent = EstimateExtent(availableSize, layoutId);
SetLayoutOrigin();
return new Size(_lastExtent.Width, _lastExtent.Height);
}
public Size Arrange(
Size finalSize,
VirtualizingLayoutContext context,
LineAlignment lineAlignment,
string layoutId)
{
ArrangeVirtualizingLayout(finalSize, lineAlignment, 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(
ILayoutable 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)
{
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
{
// 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)
{
// 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
{
// No suggestion - just pick first in realized range
anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
}
}
_firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
if (_elementManager.IsIndexValidInData(anchorIndex))
{
if (!_elementManager.IsDataIndexRealized(anchorIndex))
{
// Disconnected, throw everything and create new anchor
_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);
}
else
{
_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,
string layoutId)
{
if (anchorIndex != -1)
{
int step = (direction == GenerateDirection.Forward) ? 1 : -1;
int previousIndex = anchorIndex;
int currentIndex = anchorIndex + step;
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
var lineOffset = _orientation.MajorStart(anchorBounds);
var lineMajorSize = _orientation.MajorSize(anchorBounds);
int countInLine = 1;
int count = 0;
bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) &&
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 (_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 (_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);
}
}
}
// 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);
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 indicies inside the realization window.
if (count > 0)
{
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)
{
ILayoutable firstRealizedElement = null;
Rect firstBounds = new Rect();
ILayoutable 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);
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,
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, 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, 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,
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);
var element = _elementManager.GetAt(rangeIndex);
element.Arrange(bounds);
}
}
private void SetLayoutOrigin()
{
if (IsVirtualizingContext)
{
_context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
}
}
public ILayoutable GetElementIfRealized(int dataIndex)
{
if (_elementManager.IsDataIndexRealized(dataIndex))
{
return _elementManager.GetRealizedElement(dataIndex);
}
return null;
}
public bool TryAddElement0(ILayoutable element)
{
if (_elementManager.GetRealizedElementCount() == 0)
{
_elementManager.Add(element, 0);
return true;
}
return false;
}
public enum LineAlignment
{
Start,
Center,
End,
SpaceAround,
SpaceBetween,
SpaceEvenly,
}
private enum GenerateDirection
{
Forward,
Backward,
}
}
}

44
src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs

@ -0,0 +1,44 @@
// 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,
ILayoutable firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
ILayoutable lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds);
void Algorithm_OnElementMeasured(
ILayoutable element,
int index,
Size availableSize,
Size measureSize,
Size desiredSize,
Size provisionalArrangeSize,
VirtualizingLayoutContext context);
void Algorithm_OnLineArranged(
int startIndex,
int countInLine,
double lineSize,
VirtualizingLayoutContext context);
}
}

24
src/Avalonia.Layout/LayoutContext.cs

@ -0,0 +1,24 @@
// 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; set; }
/// <summary>
/// Implements the behavior of <see cref="LayoutState"/> in a derived or custom LayoutContext.
/// </summary>
protected virtual object LayoutStateCore { get; set; }
}
}

103
src/Avalonia.Layout/NonVirtualizingLayout.cs

@ -0,0 +1,103 @@
// 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
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
}
/// <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 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 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 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 virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
}
}

2
src/Avalonia.Controls/Orientation.cs → src/Avalonia.Layout/Orientation.cs

@ -1,7 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls
namespace Avalonia.Layout
{
/// <summary>
/// Defines vertical or horizontal orientation.

96
src/Avalonia.Layout/OrientationBasedMeasures.cs

@ -0,0 +1,96 @@
// 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);
}
}
}

336
src/Avalonia.Layout/StackLayout.cs

@ -0,0 +1,336 @@
// 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>
/// 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="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<StackLayout, Orientation>(nameof(Orientation), Orientation.Vertical);
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackLayout, double>(nameof(Spacing));
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
/// <summary>
/// Initializes a new instance of the StackLayout class.
/// </summary>
public StackLayout()
{
LayoutId = "StackLayout";
}
/// <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,
ILayoutable firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
ILayoutable 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));
}
}
return extent;
}
internal void OnElementMeasured(
ILayoutable 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,
ILayoutable firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
ILayoutable lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
return GetExtent(
availableSize,
context,
firstRealized,
firstRealizedItemIndex,
firstRealizedLayoutBounds,
lastRealized,
lastRealizedItemIndex,
lastRealizedLayoutBounds);
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable 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);
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
}
}
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
protected 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 override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
false,
0,
Spacing,
_orientation.ScrollOrientation,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
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 e)
{
if (e.Property == OrientationProperty)
{
var orientation = (Orientation)e.NewValue;
//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 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 FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState).FlowAlgorithm;
}
}

61
src/Avalonia.Layout/StackLayoutState.cs

@ -0,0 +1,61 @@
// 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 OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
}
}

520
src/Avalonia.Layout/UniformGridLayout.cs

@ -0,0 +1,520 @@
// 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>
/// 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="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;
/// <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 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;
int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
double 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 = 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 = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = indexOfFirstInLine;
var state = context.LayoutState as UniformGridLayoutState;
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,
ILayoutable firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
ILayoutable lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize);
int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
(int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
double lineSize = GetMajorSizeWithSpacing(context);
if (itemsCount > 0)
{
_orientation.SetMinorSize(
ref extent,
!double.IsInfinity(availableSizeMinor) ?
availableSizeMinor :
Math.Max(0.0, itemsCount * 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);
}
}
return extent;
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable 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 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 override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context);
}
protected 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);
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
true,
MinItemSpacing,
LineSpacing,
_orientation.ScrollOrientation,
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 new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
(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 args)
{
if (args.Property == OrientationProperty)
{
var orientation = (Orientation)args.NewValue;
//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 (args.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = (double)args.NewValue;
}
else if (args.Property == MinRowSpacingProperty)
{
_minRowSpacing = (double)args.NewValue;
}
else if (args.Property == ItemsJustificationProperty)
{
_itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
}
else if (args.Property == ItemsStretchProperty)
{
_itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
}
else if (args.Property == MinItemWidthProperty)
{
_minItemWidth = (double)args.NewValue;
}
else if (args.Property == MinItemHeightProperty)
{
_minItemHeight = (double)args.NewValue;
}
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 = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
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 FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState).FlowAlgorithm;
}
}

192
src/Avalonia.Layout/UniformGridLayoutState.cs

@ -0,0 +1,192 @@
// 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 ILayoutable _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)
{
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);
_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);
// This doesn't need to be done in the UWP version and I'm not sure why. If we
// don't do this here, and we receive a recycled element then it will be shown
// at its previous arrange point, but we don't want it shown at all until its
// arranged.
_cachedFirstElement.Arrange(new Rect(-10000.0, -10000.0, 0, 0));
SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
// 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(
ILayoutable element,
double layoutItemWidth,
double LayoutItemHeight,
Size availableSize,
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
double minColumnSpacing)
{
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;
itemSizeMinor += minorItemSpacing;
var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor));
var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor);
var extraMinorPixelsForEachItem = 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;
}
}
}
}
}

32
src/Avalonia.Layout/Utils/ListUtils.cs

@ -0,0 +1,32 @@
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));
}
}
public static void Resize<T>(this List<T> list, int count)
{
Resize(list, count, default);
}
}
}

139
src/Avalonia.Layout/VirtualizingLayout.cs

@ -0,0 +1,139 @@
// 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
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, 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>
/// <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 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 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 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 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();
}
}

190
src/Avalonia.Layout/VirtualizingLayoutContext.cs

@ -0,0 +1,190 @@
// 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
{
/// <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 ILayoutable 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 ILayoutable 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(ILayoutable)"/>, which must be implemented
/// in a derived class.
/// </remarks>
public void RecycleElement(ILayoutable 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 ILayoutable 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(ILayoutable element);
}
}

1
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -547,7 +547,6 @@ namespace Avalonia.Rendering
}
}
System.Diagnostics.Debug.WriteLine("Invalidated " + rect);
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
}

4
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@ -149,11 +149,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
[Obsolete("Don't use", true)]
public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null);
[DebuggerStepThrough]
// Don't emit debug symbols for this code so debugger will be forced to step into XAML instead
#line hidden
public static IServiceProvider CreateRootServiceProviderV2()
{
return new RootServiceProvider(new NameScope());
}
#line default
class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider
{

1
tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs

@ -8,6 +8,7 @@ using Xunit;
using System.Windows.Input;
using System;
using Avalonia.Data.Converters;
using Avalonia.Layout;
namespace Avalonia.Base.UnitTests.Data.Converters
{

1
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@ -6,6 +6,7 @@ using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;

33
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -43,6 +45,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("foo", target.SelectedItem);
}
[Fact]
public void First_Item_Should_Be_Selected_When_Reset()
{
var items = new ResetOnAdd();
var target = new TestSelector
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
items.Add("foo");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("foo", target.SelectedItem);
}
[Fact]
public void Item_Should_Be_Selected_When_Selection_Removed()
{
@ -100,5 +120,18 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectionModeProperty.OverrideDefaultValue<TestSelector>(SelectionMode.AlwaysSelected);
}
}
private class ResetOnAdd : List<string>, INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public new void Add(string item)
{
base.Add(item);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}
}

1
tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Xunit;

1
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests

1
tests/Avalonia.Controls.UnitTests/SliderTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests

3
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -93,4 +94,4 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
}
}
}
}

1
tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs

@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;

1
tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs

@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;

Loading…
Cancel
Save