diff --git a/DataBox.sln b/DataBox.sln index d918eed..4ee3b33 100644 --- a/DataBox.sln +++ b/DataBox.sln @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataBox.DataVirtualization" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataBoxDataVirtualizationDemo", "samples\DataBoxDataVirtualizationDemo\DataBoxDataVirtualizationDemo.csproj", "{51696845-5450-4A14-9368-E1063B6B14B5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualPanel", "src\VirtualPanel\VirtualPanel.csproj", "{2C2535BC-38DB-4FFF-BD30-ABAED465E7D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -89,6 +91,10 @@ Global {51696845-5450-4A14-9368-E1063B6B14B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {51696845-5450-4A14-9368-E1063B6B14B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {51696845-5450-4A14-9368-E1063B6B14B5}.Release|Any CPU.Build.0 = Release|Any CPU + {2C2535BC-38DB-4FFF-BD30-ABAED465E7D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C2535BC-38DB-4FFF-BD30-ABAED465E7D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C2535BC-38DB-4FFF-BD30-ABAED465E7D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C2535BC-38DB-4FFF-BD30-ABAED465E7D7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {17E6165A-4614-435A-8BBE-55A8C850F21B} = {BFAAFDC6-9A28-4F70-9C21-F6DF50786C70} @@ -100,5 +106,6 @@ Global {185AA19E-8CAC-4539-BD28-8199E348AC40} = {BFAAFDC6-9A28-4F70-9C21-F6DF50786C70} {8AD8D915-14C1-49E5-AB33-792C63DF2379} = {D7275F04-303E-4385-B806-08726C572D6D} {51696845-5450-4A14-9368-E1063B6B14B5} = {3B564AD4-C507-43DC-8711-38320FB3B679} + {2C2535BC-38DB-4FFF-BD30-ABAED465E7D7} = {D7275F04-303E-4385-B806-08726C572D6D} EndGlobalSection EndGlobal diff --git a/samples/VirtualPanelDemo.NetCore/Assets/avalonia-logo.ico b/samples/VirtualPanelDemo.NetCore/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/samples/VirtualPanelDemo.NetCore/Assets/avalonia-logo.ico differ diff --git a/samples/VirtualPanelDemo.NetCore/Program.cs b/samples/VirtualPanelDemo.NetCore/Program.cs new file mode 100644 index 0000000..f543c76 --- /dev/null +++ b/samples/VirtualPanelDemo.NetCore/Program.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.ReactiveUI; + +namespace VirtualPanelDemo.NetCore +{ + class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } +} diff --git a/samples/VirtualPanelDemo.NetCore/VirtualPanelDemo.NetCore.csproj b/samples/VirtualPanelDemo.NetCore/VirtualPanelDemo.NetCore.csproj new file mode 100644 index 0000000..23b665b --- /dev/null +++ b/samples/VirtualPanelDemo.NetCore/VirtualPanelDemo.NetCore.csproj @@ -0,0 +1,37 @@ + + + Exe + + + WinExe + + + net8.0 + False + enable + Debug;Release + AnyCPU;x64 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualPanelDemo/App.axaml b/samples/VirtualPanelDemo/App.axaml new file mode 100644 index 0000000..267670e --- /dev/null +++ b/samples/VirtualPanelDemo/App.axaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/VirtualPanelDemo/App.axaml.cs b/samples/VirtualPanelDemo/App.axaml.cs new file mode 100644 index 0000000..e372d04 --- /dev/null +++ b/samples/VirtualPanelDemo/App.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace VirtualPanelDemo; + +public class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime single) + { + single.MainView = new MainView(); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/samples/VirtualPanelDemo/MainView.axaml b/samples/VirtualPanelDemo/MainView.axaml new file mode 100644 index 0000000..06d5aed --- /dev/null +++ b/samples/VirtualPanelDemo/MainView.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualPanelDemo/MainView.axaml.cs b/samples/VirtualPanelDemo/MainView.axaml.cs new file mode 100644 index 0000000..288dd0b --- /dev/null +++ b/samples/VirtualPanelDemo/MainView.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using VirtualPanelDemo.ViewModels; + +namespace VirtualPanelDemo; + +public partial class MainView : UserControl +{ + public MainView() + { + InitializeComponent(); + + var vm = new MainWindowViewModel(2_000_000_000, 100, 100, 100); + + DataContext = vm; + + var vp = this.FindControl("VirtualPanel"); + + if (vm.Items is { }) + { + vm.Items.CollectionChanged += (_, _) => + { + vp.InvalidateMeasure(); + }; + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/VirtualPanelDemo/MainWindow.axaml b/samples/VirtualPanelDemo/MainWindow.axaml new file mode 100644 index 0000000..2398573 --- /dev/null +++ b/samples/VirtualPanelDemo/MainWindow.axaml @@ -0,0 +1,13 @@ + + + diff --git a/samples/VirtualPanelDemo/MainWindow.axaml.cs b/samples/VirtualPanelDemo/MainWindow.axaml.cs new file mode 100644 index 0000000..bb87ad5 --- /dev/null +++ b/samples/VirtualPanelDemo/MainWindow.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Rendering; +using VirtualPanelDemo.ViewModels; + +namespace VirtualPanelDemo; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + //RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps | RendererDebugOverlays.LayoutTimeGraph | RendererDebugOverlays.RenderTimeGraph; + RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/VirtualPanelDemo/ViewModels/ItemProvider.cs b/samples/VirtualPanelDemo/ViewModels/ItemProvider.cs new file mode 100644 index 0000000..1757a20 --- /dev/null +++ b/samples/VirtualPanelDemo/ViewModels/ItemProvider.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using DataVirtualization; + +namespace VirtualPanelDemo.ViewModels +{ + public class ItemProvider : IItemsProvider + { + private readonly int _count; + + public ItemProvider(int count) + { + _count = count; + } + + public int FetchCount() + { + return _count; + } + + public IList FetchRange(int startIndex, int pageCount, out int overallCount) + { + var result = new List(); + var endIndex = startIndex + pageCount; + + overallCount = _count; + + for (var i = startIndex; i < endIndex; i++) + { + //result.Add($"Item {i}"); + result.Add($"{i}"); + } + + return result; + } + } +} diff --git a/samples/VirtualPanelDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualPanelDemo/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..ccd5179 --- /dev/null +++ b/samples/VirtualPanelDemo/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,44 @@ +using DataVirtualization; +using ReactiveUI; + +namespace VirtualPanelDemo.ViewModels; + +public class MainWindowViewModel : ReactiveObject +{ + private AsyncVirtualizingCollection? _items; + private double _itemHeight; + private double _itemWidth; + + public AsyncVirtualizingCollection? Items + { + get => _items; + set => this.RaiseAndSetIfChanged(ref _items, value); + } + + public double ItemHeight + { + get => _itemHeight; + set => this.RaiseAndSetIfChanged(ref _itemHeight, value); + } + + public double ItemWidth + { + get => _itemWidth; + set => this.RaiseAndSetIfChanged(ref _itemWidth, value); + } + + public int Count { get; } + + public void RaiseCountChanged() + { + this.RaisePropertyChanged(nameof(Count)); + } + + public MainWindowViewModel(int count, int pageSize, int itemHeight, int itemWidth) + { + Items = new AsyncVirtualizingCollection(new ItemProvider(count), pageSize, 5000); + ItemHeight = itemHeight; + ItemWidth = itemWidth; + Count = count; + } +} diff --git a/samples/VirtualPanelDemo/VirtualPanelDemo.csproj b/samples/VirtualPanelDemo/VirtualPanelDemo.csproj new file mode 100644 index 0000000..0d59125 --- /dev/null +++ b/samples/VirtualPanelDemo/VirtualPanelDemo.csproj @@ -0,0 +1,25 @@ + + + net8.0 + Library + False + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/VirtualPanel/Properties/AssemblyInfo.cs b/src/VirtualPanel/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..af2d480 --- /dev/null +++ b/src/VirtualPanel/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "VirtualPanel")] diff --git a/src/VirtualPanel/VirtualPanel.cs b/src/VirtualPanel/VirtualPanel.cs new file mode 100644 index 0000000..f33ab58 --- /dev/null +++ b/src/VirtualPanel/VirtualPanel.cs @@ -0,0 +1,616 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Metadata; + +namespace VirtualPanel; + +public enum VirtualPanelScrollMode +{ + Smooth, + Item +} + +public enum VirtualPanelLayout +{ + Stack, + Wrap +} + +public class VirtualPanel : Control, ILogicalScrollable, IChildIndexProvider +{ + #region Util + + private int GetItemsCount(IEnumerable? items) + { + if (items is null) + { + return 0; + } + + if (items is IList list) + { + return list.Count; + } + else + { + // TODO: Support other IEnumerable types. + return 0; + } + } + + #endregion + + #region ILogicalScrollable + + private Size _extent; + private Vector _offset; + private Size _viewport; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private bool _isLogicalScrollEnabled = true; + private Size _scrollSize = new(1, 1); + private Size _pageScrollSize = new(10, 10); + private EventHandler? _scrollInvalidated; + + private Vector CoerceOffset(Vector value) + { + var scrollable = (ILogicalScrollable)this; + var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); + var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); + static double Clamp(double val, double min, double max) => val < min ? min : val > max ? max : val; + } + + Size IScrollable.Extent => _extent; + + Vector IScrollable.Offset + { + get => _offset; + set + { + _offset = CoerceOffset(value); + InvalidateMeasure(); + } + } + + Size IScrollable.Viewport => _viewport; + + bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect) + { + return false; + } + + Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control @from) + { + return null; + } + + void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) + { + _scrollInvalidated?.Invoke(this, e); + } + + bool ILogicalScrollable.CanHorizontallyScroll + { + get => _canHorizontallyScroll; + set => _canHorizontallyScroll = value; + } + + bool ILogicalScrollable.CanVerticallyScroll + { + get => _canVerticallyScroll; + set => _canVerticallyScroll = value; + } + + bool ILogicalScrollable.IsLogicalScrollEnabled => _isLogicalScrollEnabled; + + Size ILogicalScrollable.ScrollSize => _scrollSize; + + Size ILogicalScrollable.PageScrollSize => _pageScrollSize; + + event EventHandler? ILogicalScrollable.ScrollInvalidated + { + add => _scrollInvalidated += value; + remove => _scrollInvalidated -= value; + } + + protected void InvalidateScrollable() + { + if (this is not ILogicalScrollable scrollable) + { + return; + } + + scrollable.RaiseScrollInvalidated(EventArgs.Empty); + } + + #endregion + + #region IChildIndexProvider + + private EventHandler? _childIndexChanged; + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + if (child is Control control) + { + foreach (var i in _controls) + { + if (i.Value == control) + { + return i.Key; + } + } + } + + return -1; + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = GetItemsCount(ItemsSource); + return true; + } + + event EventHandler? IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + private void RaiseChildIndexChanged() + { + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(null, -1)); + } + + #endregion + + #region Properties + + public static readonly StyledProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout)); + + public static readonly StyledProperty ScrollModeProperty = + AvaloniaProperty.Register(nameof(ScrollMode)); + + public static readonly StyledProperty ItemHeightProperty = + AvaloniaProperty.Register(nameof(ItemHeight), double.NaN); + + public static readonly StyledProperty ItemWidthProperty = + AvaloniaProperty.Register(nameof(ItemWidth), double.NaN); + + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); + + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItem), + o => o.SelectedItem, + (o, v) => o.SelectedItem = v, + defaultBindingMode: BindingMode.TwoWay); + + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + private object? _selectedItem; + + public VirtualPanelLayout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + public VirtualPanelScrollMode ScrollMode + { + get => GetValue(ScrollModeProperty); + set => SetValue(ScrollModeProperty, value); + } + + public double ItemHeight + { + get => GetValue(ItemHeightProperty); + set => SetValue(ItemHeightProperty, value); + } + + public double ItemWidth + { + get => GetValue(ItemWidthProperty); + set => SetValue(ItemWidthProperty, value); + } + + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public object? SelectedItem + { + get => _selectedItem; + set => SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + } + + [Content] + public IDataTemplate? ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + #endregion + + #region Events + + protected virtual void OnContainerMaterialized(Control container, int index) + { + // System.Diagnostics.Debug.WriteLine($"[Materialized] {index}, {container.DataContext}"); + } + + protected virtual void OnContainerDematerialized(Control container, int index) + { + // System.Diagnostics.Debug.WriteLine($"[Dematerialized] {index}, {container.DataContext}"); + } + + protected virtual void OnContainerRecycled(Control container, int index) + { + // System.Diagnostics.Debug.WriteLine($"[Recycled] {index}, {container.DataContext}"); + } + + #endregion + + #region Layout + + private int _startIndex = -1; + private int _endIndex = -1; + private int _visibleCount = -1; + private double _scrollOffset; + private readonly Stack _recycled = new(); + private readonly SortedDictionary _controls = new(); + private List _children = new(); + + public IReadOnlyList Children => _children; + + protected Size UpdateScrollable(double width, double height, double totalWidth) + { + var itemCount = GetItemsCount(ItemsSource); + var layout = Layout; + var itemHeight = ItemHeight; + var itemWidth = ItemWidth; + + double totalHeight; + + switch (layout) + { + case VirtualPanelLayout.Stack: + totalHeight = itemCount * itemHeight; + break; + case VirtualPanelLayout.Wrap: + var itemsPerRow = (int)(width / itemWidth); + if (itemsPerRow <= 0) + { + itemsPerRow = 1; + } + totalHeight = Math.Ceiling((double)itemCount / itemsPerRow) * itemHeight; + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var extent = new Size(totalWidth, totalHeight); + + _viewport = new Size(width, height); + _extent = extent; + _scrollSize = new Size(16, 16); + _pageScrollSize = new Size(_viewport.Width, _viewport.Height); + + return extent; + } + + private void AddChild(Control control) + { + LogicalChildren.Add(control); + VisualChildren.Add(control); + _children.Add(control); + } + + private void RemoveChildren(HashSet controls) + { + LogicalChildren.RemoveAll(controls); + VisualChildren.RemoveAll(controls); + _children.RemoveAll(controls.Contains); + } + + private void ClearChildren() + { + LogicalChildren.Clear(); + VisualChildren.Clear(); + _children.Clear(); + } + + private void InvalidateChildren(double width, double height, double offset) + { + // TODO: Support other IEnumerable types. + if (ItemsSource is not IList items) + { + _scrollOffset = 0; + return; + } + + var itemCount = GetItemsCount(items); + + var layout = Layout; + var itemHeight = ItemHeight; + var itemWidth = ItemWidth; + + _scrollOffset = ScrollMode == VirtualPanelScrollMode.Smooth ? offset % itemHeight : 0.0; + + var size = height + _scrollOffset; + + var itemsPerRow = (int)(width / itemWidth); + + if (itemsPerRow <= 0) + { + itemsPerRow = 1; + } + + switch (layout) + { + case VirtualPanelLayout.Stack: + _startIndex = (int)(offset / itemHeight); + _visibleCount = (int)(size / itemHeight); + break; + case VirtualPanelLayout.Wrap: + _startIndex = (int)(offset / itemHeight) * itemsPerRow; + _visibleCount = (int)(size / itemHeight) * itemsPerRow; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (size % itemHeight > 0 && height > 0) + { + _visibleCount += 1; + } + + switch (layout) + { + case VirtualPanelLayout.Stack: + _endIndex = (_startIndex + _visibleCount) - 1; + break; + case VirtualPanelLayout.Wrap: + _endIndex = (_startIndex + _visibleCount + itemsPerRow) - 1; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (itemCount == 0 || ItemTemplate is null) + { + ClearChildren(); + RaiseChildIndexChanged(); + return; + } + + InvalidateContainers(items, itemCount); + RaiseChildIndexChanged(); + } + + private void InvalidateContainers(IList items, int itemCount) + { + if (_startIndex >= itemCount) + { + return; + } + + if (ItemTemplate is null) + { + return; + } + + var toRemove = new List(); + + foreach (var control in _controls) + { + if (control.Key < _startIndex || control.Key > _endIndex) + { + toRemove.Add(control.Key); + } + } + + var childrenRemove = new HashSet(); + + foreach (var remove in toRemove) + { + var control = _controls[remove]; + control.DataContext = null; + _recycled.Push(control); + _controls.Remove(remove); + childrenRemove.Add(control); + OnContainerDematerialized(control, remove); + } + + for (var i = _startIndex; i <= _endIndex; i++) + { + if (i < 0 || i >= itemCount) + { + break; + } + + if (_controls.ContainsKey(i)) + { + continue; + } + + Control control; + var param = items[i]; + + if (_recycled.Count > 0) + { + control = _recycled.Pop(); + control.DataContext = param; + _controls[i] = control; + if (!childrenRemove.Contains(control)) + { + AddChild(control); + } + else + { + childrenRemove.Remove(control); + } + OnContainerRecycled(control, i); + } + else + { + var content = param is null ? null : ItemTemplate.Build(param); + control = new ContentControl + { + Content = content + }; + control.DataContext = param; + _controls[i] = control; + AddChild(control); + OnContainerMaterialized(control, i); + } + } + + RemoveChildren(childrenRemove); + } + + protected override Size MeasureOverride(Size availableSize) + { + availableSize = UpdateScrollable(availableSize.Width, availableSize.Height, availableSize.Width); + + InvalidateChildren(_viewport.Width, _viewport.Height, _offset.Y); + + if (_controls.Count > 0) + { + var layout = Layout; + var itemHeight = ItemHeight; + var itemWidth = ItemWidth; + + foreach (var control in _controls) + { + switch (layout) + { + case VirtualPanelLayout.Stack: + { + var size = new Size(_viewport.Width, ItemHeight); + control.Value.Measure(size); + break; + } + case VirtualPanelLayout.Wrap: + { + var size = new Size(itemWidth, itemHeight); + control.Value.Measure(size); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + } + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + finalSize = UpdateScrollable(finalSize.Width, finalSize.Height, finalSize.Width); + + var layout = Layout; + var itemHeight = ItemHeight; + var itemWidth = ItemWidth; + + InvalidateChildren(_viewport.Width, _viewport.Height, _offset.Y); + InvalidateScrollable(); + + var scrollOffsetX = 0.0; // TODO: _offset.X; + var scrollOffsetY = _scrollOffset; + + if (_controls.Count > 0) + { + var x = scrollOffsetX == 0.0 ? 0.0 : -scrollOffsetX; + var y = scrollOffsetY == 0.0 ? 0.0 : -scrollOffsetY; + + switch (layout) + { + case VirtualPanelLayout.Stack: + { + foreach (var control in _controls) + { + var rect = new Rect(new Point(x, y), new Size(_viewport.Width, itemHeight)); + control.Value.Arrange(rect); + y += itemHeight; + } + break; + } + case VirtualPanelLayout.Wrap: + { + var column = 0; + var itemsPerRow = (int)(_viewport.Width / itemWidth); + + foreach (var control in _controls) + { + var rect = new Rect(new Point(x + itemWidth * column, y), new Size(itemWidth, itemHeight)); + control.Value.Arrange(rect); + + column += 1; + if (column >= itemsPerRow) + { + y += itemHeight; + column = 0; + } + } + + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + return finalSize; + } + + #endregion + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == LayoutProperty) + { + InvalidateMeasure(); + } + + if (change.Property == ScrollModeProperty) + { + InvalidateMeasure(); + } + + if (change.Property == ItemsSourceProperty) + { + InvalidateMeasure(); + } + + if (change.Property == ItemWidthProperty) + { + InvalidateMeasure(); + } + + if (change.Property == ItemHeightProperty) + { + InvalidateMeasure(); + } + } +} diff --git a/src/VirtualPanel/VirtualPanel.csproj b/src/VirtualPanel/VirtualPanel.csproj new file mode 100644 index 0000000..12ae63c --- /dev/null +++ b/src/VirtualPanel/VirtualPanel.csproj @@ -0,0 +1,23 @@ + + + Library + netstandard2.0;net461;net6.0;net8.0 + False + enable + Debug;Release + AnyCPU;x64 + + + + VirtualPanel + A virtualizing smooth scrolling panel control. + virtualizing;smooth;scrolling;panel;control;xaml;axaml;avalonia;avaloniaui + + + + + + + + +