diff --git a/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.axaml b/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.axaml new file mode 100644 index 0000000..8087360 --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.cs b/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.cs new file mode 100644 index 0000000..7415192 --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/OverflowListBox/OverflowListBox.cs @@ -0,0 +1,239 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Metadata; +using Avalonia.Threading; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Toolkit.UI.Controls.Avalonia; + +public class OverflowListBox : + TemplatedControl +{ + public static readonly StyledProperty> ItemsPanelProperty = + AvaloniaProperty.Register>(nameof(ItemsPanel), new FuncTemplate(() => new StackPanel())); + + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); + + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + public static readonly StyledProperty SelectedItemProperty = + AvaloniaProperty.Register(nameof(SelectedItem), BindingMode.TwoWay); + + private readonly ObservableCollection primaryCollection = new(); + private readonly ObservableCollection secondaryCollection = new(); + + private ListBox? primaryListBox; + private ListBox? secondaryListBox; + + public ITemplate ItemsPanel + { + get => GetValue(ItemsPanelProperty); + set => SetValue(ItemsPanelProperty, value); + } + + public IEnumerable? ItemsSource + { + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IDataTemplate? ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + public object? SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs args) + { + base.OnApplyTemplate(args); + + primaryListBox = args.NameScope.Get("PrimaryListBox"); + primaryListBox?.SetValue(ItemsControl.ItemsSourceProperty, primaryCollection); + + secondaryListBox = args.NameScope.Get("SecondaryListBox"); + secondaryListBox?.SetValue(ItemsControl.ItemsSourceProperty, secondaryCollection); + + InitializeCollections(); + UpdateOverflow(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + base.OnPropertyChanged(args); + if (args.Property == ItemsSourceProperty) + { + if (args.OldValue is IEnumerable oldCollection && oldCollection is INotifyCollectionChanged oldNotifyCollectionChanged) + { + oldNotifyCollectionChanged.CollectionChanged -= OnSourceCollectionChanged; + } + + if (args.NewValue is IEnumerable newCollection && newCollection is INotifyCollectionChanged notifyCollectionChanged) + { + notifyCollectionChanged.CollectionChanged += OnSourceCollectionChanged; + } + + InitializeCollections(); + UpdateOverflow(); + } + } + + private void InitializeCollections() + { + primaryCollection.Clear(); + secondaryCollection.Clear(); + + if (ItemsSource is not null) + { + foreach (object? item in ItemsSource) + { + primaryCollection.Add(item); + } + } + } + + private void OnSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + if (args.NewItems is not null) + { + int insertIndex = args.NewStartingIndex > primaryCollection.Count ? primaryCollection.Count : args.NewStartingIndex; + foreach (object? newItem in args.NewItems) + { + primaryCollection.Insert(insertIndex++, newItem); + } + } + break; + + case NotifyCollectionChangedAction.Remove: + if (args.OldItems is not null) + { + foreach (object? oldItem in args.OldItems) + { + primaryCollection.Remove(oldItem); + secondaryCollection.Remove(oldItem); + } + } + break; + + case NotifyCollectionChangedAction.Replace: + if (args.OldItems is not null && args.NewItems is not null && args.OldItems.Count == args.NewItems.Count) + { + for (int i = 0; i < args.OldItems.Count; i++) + { + if (args.OldItems[i] is object oldItem && + args.NewItems[i] is object newItem) + { + int index = primaryCollection.IndexOf(oldItem); + if (index != -1) + { + primaryCollection[index] = newItem; + } + + index = secondaryCollection.IndexOf(oldItem); + if (index != -1) + { + secondaryCollection[index] = newItem; + } + } + } + } + break; + + case NotifyCollectionChangedAction.Move: + if (args.OldItems != null && args.NewItems != null && args.OldItems.Count == args.NewItems.Count) + { + for (int i = 0; i < args.OldItems.Count; i++) + { + if (args.OldItems[i] is object item) + { + int oldIndex = primaryCollection.IndexOf(item); + if (oldIndex != -1) + { + primaryCollection.RemoveAt(oldIndex); + + int newIndex = args.NewStartingIndex + i; + primaryCollection.Insert(newIndex, item); + } + + oldIndex = secondaryCollection.IndexOf(item); + if (oldIndex != -1) + { + secondaryCollection.RemoveAt(oldIndex); + + int newIndex = args.NewStartingIndex + i; + secondaryCollection.Insert(newIndex, item); + } + } + } + } + break; + + case NotifyCollectionChangedAction.Reset: + InitializeCollections(); + break; + } + + UpdateOverflow(); + } + + private void UpdateOverflow() + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (ItemsSource is null) + { + return; + } + + double controlWidth = primaryListBox?.DesiredSize.Width ?? 0; + double accumulatedWidth = 0; + double itemSpacing = 6; + + List<(object item, int originalIndex)> itemsToMoveToSecondary = new(); + + for (int i = 0; i < primaryCollection.Count; i++) + { + object? item = primaryCollection[i]; + if (item is not null && primaryListBox?.ContainerFromItem(item) is ListBoxItem itemContainer) + { + itemContainer.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + double itemWidth = itemContainer.DesiredSize.Width; + + if (accumulatedWidth + itemWidth + (itemsToMoveToSecondary.Count * itemSpacing) > controlWidth) + { + itemsToMoveToSecondary.Add((item, i)); + } + else + { + accumulatedWidth += itemWidth + itemSpacing; + } + } + } + + foreach (var (item, originalIndex) in itemsToMoveToSecondary.OrderByDescending(x => x.originalIndex)) + { + primaryCollection.Remove(item); + int insertIndexInSecondary = originalIndex - primaryCollection.Count; + secondaryCollection.Insert(insertIndexInSecondary, item); + } + }); + } + +} + diff --git a/Toolkit.UI.Controls.Avalonia/Themes/ControlResources.axaml b/Toolkit.UI.Controls.Avalonia/Themes/ControlResources.axaml index 8003c5d..76bc8a4 100644 --- a/Toolkit.UI.Controls.Avalonia/Themes/ControlResources.axaml +++ b/Toolkit.UI.Controls.Avalonia/Themes/ControlResources.axaml @@ -6,6 +6,7 @@ +