using System.Collections; using System.Collections.Specialized; using Avalonia; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using GSS2.Core.Extentions; namespace GSS2.UI.Core; public class SectionPanel : Panel { public static readonly StyledProperty CurrentIndexProperty = AvaloniaProperty.Register( name: nameof(CurrentIndex), defaultValue: 0, coerce: (obj, value) => { if (obj is not SectionPanel sectionPanel) return value; if (sectionPanel.Cyclic) return value; if (sectionPanel.Children.Count() == 0) return 0; return Math.Clamp(value, 0, sectionPanel.Children.Count() - 1); }); public int CurrentIndex { get => GetValue(CurrentIndexProperty); set => SetValue(CurrentIndexProperty, value); } public static readonly StyledProperty EnableAnimationProperty = AvaloniaProperty.Register(nameof(EnableAnimation), true); public bool EnableAnimation { get => GetValue(EnableAnimationProperty); set => SetValue(EnableAnimationProperty, value); } public static readonly StyledProperty AnimationSpeedProperty = AvaloniaProperty.Register(nameof(AnimationSpeed), 500); public double AnimationSpeed { get => GetValue(AnimationSpeedProperty); set => SetValue(AnimationSpeedProperty, value); } public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); public Orientation Orientation { get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } public static readonly StyledProperty CyclicProperty = AvaloniaProperty.Register(nameof(Cyclic), false); public bool Cyclic { get => GetValue(CyclicProperty); set => SetValue(CyclicProperty, value); } public static readonly StyledProperty CanScrollForwardProperty = AvaloniaProperty.Register(nameof(CanScrollForward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource); public bool CanScrollForward { get => GetValue(CanScrollForwardProperty); set => SetValue(CanScrollForwardProperty, value); } public static readonly StyledProperty CanScrollBackwardProperty = AvaloniaProperty.Register(nameof(CanScrollBackward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource); public bool CanScrollBackward { get => GetValue(CanScrollBackwardProperty); set => SetValue(CanScrollBackwardProperty, value); } public static readonly StyledProperty ChildrenSourceProperty = AvaloniaProperty.Register(nameof(ChildrenSource), null, defaultBindingMode: Avalonia.Data.BindingMode.OneWay); public IEnumerable? ChildrenSource { get => GetValue(ChildrenSourceProperty); set => SetValue(ChildrenSourceProperty, value); } public static readonly StyledProperty ChildrenTemplateProperty = AvaloniaProperty.Register(nameof(ChildrenTemplate), null); public IDataTemplate? ChildrenTemplate { get => GetValue(ChildrenTemplateProperty); set => SetValue(ChildrenTemplateProperty, value); } private readonly Dictionary _childIndeces = new Dictionary(); private bool _initialized = false; private int _previousIndex = 0; public SectionPanel() : base() { CurrentIndexProperty.Changed.AddClassHandler((panel, ea) => { CanScrollForward = Cyclic || CurrentIndex != Children.Count() - 1; CanScrollBackward = Cyclic || CurrentIndex != 0; panel.InvalidateArrange(); }); ChildrenSourceProperty.Changed.AddClassHandler((panel, ea) => { if (panel != this) return; (ea.OldValue as INotifyCollectionChanged)?.CollectionChanged -= panel.OnChildrenSourceCollectionChanged; (ea.NewValue as INotifyCollectionChanged)?.CollectionChanged += panel.OnChildrenSourceCollectionChanged; panel.Children.Clear(); if (ea.NewValue is IEnumerable enumerable) { IEnumerable controls; if (ChildrenTemplate is null) controls = enumerable.OfType(); else controls = enumerable.Cast() .Where(ChildrenTemplate.Match) .Select(i => { var control = ChildrenTemplate.Build(i); control?.DataContext = i; return control; }) .OfType(); foreach (var control in controls) panel.Children.Add(control); if (_initialized) { _childIndeces.Clear(); foreach (var (i, child) in Children.Enumerate()) _childIndeces.Add(child, i); _initialized = true; } } panel.InvalidateArrange(); }); Children.CollectionChanged += (_, ea) => { CanScrollForward = (Cyclic && Children.Count() > 1) || CurrentIndex != Children.Count() - 1; CanScrollBackward = (Cyclic && Children.Count() > 1) || CurrentIndex != 0; }; } protected void OnChildrenSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs ea) { switch (ea.Action) { case NotifyCollectionChangedAction.Add: { if (ea.NewItems is null) return; var index = ea.NewStartingIndex >= 0 ? ea.NewStartingIndex : Children.Count; IEnumerable controls; if (ChildrenTemplate is null) controls = ea.NewItems.OfType(); else controls = ea.NewItems.Cast() .Where(ChildrenTemplate.Match) .Select(i => { var control = ChildrenTemplate.Build(i); control?.DataContext = i; return control; }) .OfType(); foreach (Control control in controls) Children.Insert(index++, control); break; } case NotifyCollectionChangedAction.Remove: { if (ea.OldItems is null) return; foreach (Control control in ea.OldItems) Children.Remove(control); break; } case NotifyCollectionChangedAction.Replace: { if (ea.OldItems is not null) foreach (Control control in ea.OldItems) Children.Remove(control); if (ea.NewItems is not null) { var index = ea.NewStartingIndex >= 0 ? ea.NewStartingIndex : Children.Count; foreach (Control control in ea.NewItems) Children.Insert(index++, control); } break; } case NotifyCollectionChangedAction.Move: { if (ea.OldItems is not IEnumerable oldControls || oldControls.Count() == 0) return; var control = oldControls.First(); if (ea.OldStartingIndex >= 0) Children.RemoveAt(ea.OldStartingIndex); if (ea.NewStartingIndex >= 0) Children.Insert(ea.NewStartingIndex, control); break; } case NotifyCollectionChangedAction.Reset: { Children.Clear(); if (ChildrenSource is null) break; IEnumerable controls; if (ChildrenTemplate is null) controls = ChildrenSource.OfType(); else controls = ChildrenSource.Cast() .Where(ChildrenTemplate.Match) .Select(i => { var control = ChildrenTemplate.Build(i); control?.DataContext = i; return control; }) .OfType(); foreach (var control in controls) Children.Add(control); break; } } InvalidateMeasure(); InvalidateArrange(); } protected override void OnSizeChanged(SizeChangedEventArgs e) { if (!_initialized || e.PreviousSize.Width == 0 || e.PreviousSize.Height == 0) { base.OnSizeChanged(e); return; } Console.WriteLine($"{e.NewSize} {e.PreviousSize}"); foreach (var (child, i) in _childIndeces) { var transform = child.RenderTransform as TranslateTransform; if (transform is null) { transform = new TranslateTransform(); child.RenderTransform = transform; } Console.WriteLine($"{i} {transform}"); if (child.RenderTransformOrigin != RelativePoint.TopLeft) child.RenderTransformOrigin = RelativePoint.TopLeft; _ = Orientation switch { Orientation.Vertical => transform.Y += transform.Y == 0 ? 0 : e.PreviousSize.Height - e.NewSize.Height, Orientation.Horizontal => transform.X += transform.X == 0 ? 0 : e.PreviousSize.Width - e.NewSize.Width, _ => 0 }; Console.WriteLine($"{i} {transform}"); } base.OnSizeChanged(e); } protected override Size MeasureOverride(Size availableSize) { foreach (var child in Children) child.Measure(availableSize); return availableSize; } protected override Size ArrangeOverride(Size finalSize) { if (!_initialized) { foreach (var (i, child) in Children.Enumerate()) _childIndeces.Add(child, i); _initialized = true; } foreach (var (child, i) in _childIndeces) { var rect = Orientation switch { Orientation.Vertical => new Rect(0, i * finalSize.Height, finalSize.Width, finalSize.Height), Orientation.Horizontal => new Rect(i * finalSize.Width, 0, finalSize.Width, finalSize.Height), _ => default }; child.Arrange(rect); } if (Children.Count() > 1) Dispatcher.UIThread.Post(async () => _ = ApplyOffset(finalSize, true), DispatcherPriority.Render); return finalSize; } private async Task ApplyOffset(Size size, bool animate) { int indexShift = CurrentIndex - _previousIndex; while (indexShift != 0) { int indexSingleShift = indexShift / Math.Abs(indexShift); var firstIndex = _childIndeces.Min(ch => ch.Value); var lastIndex = _childIndeces.Max(ch => ch.Value); if (indexSingleShift == -1 && _previousIndex - 1 < firstIndex) { var newIndex = firstIndex - 1; var (child, _) = _childIndeces.MaxBy(ch => ch.Value); InstantTransform(child, -Children.Count(), size); _childIndeces[child] = newIndex; } if (indexSingleShift == 1 && _previousIndex + 1 > lastIndex) { var newIndex = lastIndex + 1; var (child, _) = _childIndeces.MinBy(ch => ch.Value); InstantTransform(child, Children.Count(), size); _childIndeces[child] = newIndex; } List animations = new List(); foreach (var (i, child) in Children.Enumerate()) { double offset = Orientation switch { Orientation.Vertical => -indexSingleShift * size.Height, Orientation.Horizontal => -indexSingleShift * size.Width, _ => 0 }; if (!animate || !EnableAnimation) InstantTransform(child, offset); else animations.Add(AnimationTransform(child, offset, Math.Abs(indexShift))); } if (animations.Count() > 0) await Task.WhenAll(animations); indexShift -= indexSingleShift; _previousIndex += indexSingleShift; } } private void InstantTransform(Control control, double offset, Size? size = null) { var transform = control.RenderTransform as TranslateTransform; if (transform is null) { transform = new TranslateTransform(); control.RenderTransform = transform; } if (control.RenderTransformOrigin != RelativePoint.TopLeft) control.RenderTransformOrigin = RelativePoint.TopLeft; if (Orientation is Orientation.Vertical) transform.Y += offset * (size is null ? 1 : size.Value.Height); else transform.X += offset * (size is null ? 1 : size.Value.Width); } private Task AnimationTransform(Control control, double offset, double speedRatio) { var transform = control.RenderTransform as TranslateTransform; if (transform is null) { transform = new TranslateTransform(); control.RenderTransform = transform; } if (control.RenderTransformOrigin != RelativePoint.TopLeft) control.RenderTransformOrigin = RelativePoint.TopLeft; var animation = new Animation { Duration = TimeSpan.FromMilliseconds(Math.Clamp(AnimationSpeed / speedRatio, 120, 1000)), FillMode = FillMode.Forward, Children = { new KeyFrame { Cue = new Cue(1), Setters = { Orientation is Orientation.Vertical ? new Setter(TranslateTransform.YProperty, transform.Y + offset) : new Setter(TranslateTransform.XProperty, transform.X + offset) } } } }; return animation.RunAsync(control); } }