Files
GSS2Rework/GSS2.UI.Core/SectionPanel.cs
T
2026-03-05 07:36:17 +03:00

436 lines
16 KiB
C#

using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
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<int> CurrentIndexProperty = AvaloniaProperty.Register<SectionPanel, int>(
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<bool> EnableAnimationProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(EnableAnimation), true);
public bool EnableAnimation
{
get => GetValue(EnableAnimationProperty);
set => SetValue(EnableAnimationProperty, value);
}
public static readonly StyledProperty<double> AnimationSpeedProperty = AvaloniaProperty.Register<SectionPanel, double>(nameof(AnimationSpeed), 500);
public double AnimationSpeed
{
get => GetValue(AnimationSpeedProperty);
set => SetValue(AnimationSpeedProperty, value);
}
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<SectionPanel, Orientation>(nameof(Orientation), Orientation.Vertical);
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public static readonly StyledProperty<bool> CyclicProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(Cyclic), false);
public bool Cyclic
{
get => GetValue(CyclicProperty);
set => SetValue(CyclicProperty, value);
}
public static readonly StyledProperty<bool> CanScrollForwardProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(CanScrollForward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource);
public bool CanScrollForward
{
get => GetValue(CanScrollForwardProperty);
set => SetValue(CanScrollForwardProperty, value);
}
public static readonly StyledProperty<bool> CanScrollBackwardProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(CanScrollBackward), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource);
public bool CanScrollBackward
{
get => GetValue(CanScrollBackwardProperty);
set => SetValue(CanScrollBackwardProperty, value);
}
public static readonly StyledProperty<IEnumerable?> ChildrenSourceProperty = AvaloniaProperty.Register<SectionPanel, IEnumerable?>(nameof(ChildrenSource), null, defaultBindingMode: Avalonia.Data.BindingMode.OneWay);
public IEnumerable? ChildrenSource
{
get => GetValue(ChildrenSourceProperty);
set => SetValue(ChildrenSourceProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> ChildrenTemplateProperty = AvaloniaProperty.Register<SectionPanel, IDataTemplate?>(nameof(ChildrenTemplate), null);
public IDataTemplate? ChildrenTemplate
{
get => GetValue(ChildrenTemplateProperty);
set => SetValue(ChildrenTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsScrollingProperty = AvaloniaProperty.Register<SectionPanel, bool>(nameof(IsScrolling), false, defaultBindingMode: Avalonia.Data.BindingMode.OneWayToSource);
public bool IsScrolling
{
get => GetValue(IsScrollingProperty);
set => SetValue(IsScrollingProperty, value);
}
private readonly Dictionary<Control, int> _childIndeces = new Dictionary<Control, int>();
private bool _initialized = false;
private int _previousIndex = 0;
public SectionPanel()
: base()
{
CurrentIndexProperty.Changed.AddClassHandler<SectionPanel>((panel, ea) =>
{
if (panel != this)
return;
panel.CanScrollBackward = panel.Cyclic || panel.CurrentIndex != 0;
panel.CanScrollForward = panel.Cyclic || panel.CurrentIndex != panel.Children.Count() - 1;
panel.InvalidateArrange();
});
ChildrenSourceProperty.Changed.AddClassHandler<SectionPanel>((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<Control> controls;
if (ChildrenTemplate is null)
controls = enumerable.OfType<Control>();
else
controls = enumerable.Cast<object?>()
.Where(ChildrenTemplate.Match)
.Select((vm, i) =>
{
var control = ChildrenTemplate.Build(i);
control?.DataContext = vm;
control?.Name = $"Элемент {i}";
return control;
})
.OfType<Control>();
foreach (var control in controls)
panel.Children.Add(control);
if (_initialized)
{
_childIndeces.Clear();
foreach (var (i, child) in Children.Enumerate())
_childIndeces.Add(child, i);
}
}
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<Control> controls;
if (ChildrenTemplate is null)
controls = ea.NewItems.OfType<Control>();
else
controls = ea.NewItems.Cast<object?>()
.Where(ChildrenTemplate.Match)
.Select((vm, i) =>
{
var control = ChildrenTemplate.Build(i);
control?.DataContext = vm;
control?.Name = $"Элемент {i}";
return control;
})
.OfType<Control>();
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<Control> 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<Control> controls;
if (ChildrenTemplate is null)
controls = ChildrenSource.OfType<Control>();
else
controls = ChildrenSource.Cast<object?>()
.Where(ChildrenTemplate.Match)
.Select((vm, i) =>
{
var control = ChildrenTemplate.Build(i);
control?.DataContext = vm;
control?.Name = $"Элемент {i}";
return control;
})
.OfType<Control>();
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;
}
foreach (var (child, i) in _childIndeces)
{
var transform = child.RenderTransform as TranslateTransform;
if (transform is null)
{
transform = new TranslateTransform();
child.RenderTransform = 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
};
}
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;
}
if (_previousIndex == CurrentIndex)
{
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);
}
}
else 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<Task> animations = new List<Task>();
double offset = Orientation switch
{
Orientation.Vertical => -indexSingleShift * size.Height,
Orientation.Horizontal => -indexSingleShift * size.Width,
_ => 0
};
foreach (var (i, child) in Children.Enumerate())
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;
_ = Orientation switch
{
Orientation.Vertical => transform.Y += offset * (size is null ? 1 : size.Value.Height),
Orientation.Horizontal => transform.X += offset * (size is null ? 1 : size.Value.Width),
_ => throw new UnreachableException(),
};
}
private Task AnimationTransform(Control control, double offset, double speedRatio, 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;
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 switch
{
Orientation.Vertical => new Setter(TranslateTransform.YProperty, transform.Y + offset * (size is null ? 1 : size.Value.Height)),
Orientation.Horizontal => new Setter(TranslateTransform.XProperty, transform.X + offset * (size is null ? 1 : size.Value.Width)),
_ => throw new UnreachableException(),
}
}
}
}
};
IsScrolling = true;
return animation.RunAsync(control).ContinueWith(_ => Dispatcher.UIThread.Invoke(() => IsScrolling = false));
}
}