forked from amkovkov/GranuSightSoftware2
436 lines
16 KiB
C#
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.Extensions;
|
|
|
|
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> _childIndices = 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)
|
|
{
|
|
_childIndices.Clear();
|
|
foreach (var (i, child) in Children.Enumerate())
|
|
_childIndices.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 _childIndices)
|
|
{
|
|
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())
|
|
_childIndices.Add(child, i);
|
|
_initialized = true;
|
|
}
|
|
|
|
if (_previousIndex == CurrentIndex)
|
|
{
|
|
foreach (var (child, i) in _childIndices)
|
|
{
|
|
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 = _childIndices.Min(ch => ch.Value);
|
|
var lastIndex = _childIndices.Max(ch => ch.Value);
|
|
|
|
if (indexSingleShift == -1 &&
|
|
_previousIndex - 1 < firstIndex)
|
|
{
|
|
var newIndex = firstIndex - 1;
|
|
var (child, _) = _childIndices.MaxBy(ch => ch.Value);
|
|
InstantTransform(child, -Children.Count(), size);
|
|
|
|
_childIndices[child] = newIndex;
|
|
}
|
|
|
|
if (indexSingleShift == 1 &&
|
|
_previousIndex + 1 > lastIndex)
|
|
{
|
|
var newIndex = lastIndex + 1;
|
|
var (child, _) = _childIndices.MinBy(ch => ch.Value);
|
|
InstantTransform(child, Children.Count(), size);
|
|
|
|
_childIndices[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));
|
|
}
|
|
} |