canto/Assets/Scripts/UI/Elements/Recycler.cs

338 lines
7.4 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using KitsuneCafe.UI.MVVM;
using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public class RecyclerViewModel
{
public record RotateEvent(Direction Direction, int FirstIndex, object CurrentItem);
public enum Direction
{
None = 0,
Clockwise = -1,
CounterClockwise = 1
}
private int displayCount;
public int DisplayCount
{
get => displayCount;
set
{
if (displayCount != value)
{
displayCount = value;
}
}
}
private IList itemSource;
public IList ItemSource
{
get => itemSource;
set
{
if (itemSource != value)
{
itemSource = value;
}
}
}
public int Count => Math.Max(displayCount, itemSource.Count);
private int currentIndex = 0;
public int SelectedIndex => currentIndex;
public readonly ICommand<Direction> RotateCommand;
public event EventHandler<RotateEvent> Rotated = delegate { };
public RecyclerViewModel()
{
RotateCommand = new RelayCommand<Direction>(CanRotate, Rotate);
}
private bool CanRotate(Direction direction)
{
return direction == Direction.Clockwise || direction == Direction.CounterClockwise;
}
public static int WrapIndex(int index, int count)
{
return (index + count) % count;
}
public int WrapIndex(int index)
{
return WrapIndex(index, Count);
}
public int GetOffset(int index)
{
return WrapIndex(currentIndex + index);
}
public static T GetItem<T>(IList<T> xs, int index)
{
return xs[WrapIndex(index, xs.Count)];
}
public static object GetItem(IList xs, int index)
{
return xs[WrapIndex(index, xs.Count)];
}
public object GetItem(int index)
{
if (0 > index || index >= itemSource.Count) { return null; }
return GetItem(itemSource, index);
}
public static IEnumerable<T> GetSlice<T>(IList<T> xs, int first, int count)
{
var diff = count - xs.Count;
var xs2 = new List<T>(xs);
xs2.AddRange(Enumerable.Repeat<T>(default, diff));
var len = first + count;
for (int i = first; i < len; i++)
{
yield return GetItem<T>(xs2, i);
}
}
public static IEnumerable GetSlice(IList xs, int first, int count)
{
var diff = count - xs.Count;
var xs2 = xs.Cast<object>().ToList();
xs2.AddRange(Enumerable.Repeat<object>(default, diff));
var len = first + count;
for (int i = first; i < len; i++)
{
yield return GetItem<object>(xs2, i);
}
}
public IEnumerable GetSlice(int first, int count)
{
return GetSlice(itemSource, first, count);
}
public void Rotate(Direction direction)
{
var dir = direction switch
{
Direction.Clockwise => 1,
Direction.CounterClockwise => -1,
_ => 0
};
if (dir != 0)
{
currentIndex = WrapIndex(currentIndex + dir);
Notify(direction);
}
}
private void Notify(Direction direction)
{
Rotated?.Invoke(this, new RotateEvent(
direction,
currentIndex,
GetItem(currentIndex)
));
}
}
public class SelectEvent : EventBase<SelectEvent>
{
public int SelectedIndex;
public object SelectedItem;
protected override void Init()
{
base.Init();
bubbles = true;
tricklesDown = true;
}
}
[UxmlElement]
public partial class RecyclerView : VisualElement
{
public const string BaseClass = "kitsunecafe__recycler-view";
[UxmlAttribute, CreateProperty]
public int DisplayCount
{
get => viewModel.DisplayCount;
set
{
viewModel.DisplayCount = value;
CreateItems();
}
}
[CreateProperty]
public IList ItemSource
{
get => viewModel.ItemSource;
set
{
if (ItemSource != value)
{
viewModel.ItemSource = value;
CreateItems();
Rebind();
}
}
}
private VisualTreeAsset template;
[UxmlAttribute, CreateProperty]
public VisualTreeAsset Template
{
get => template;
set
{
if (template != value)
{
template = value;
CreateItems();
Rebind();
}
}
}
private readonly RecyclerViewModel viewModel = new();
private readonly DragManipulator drag;
public RecyclerView()
{
AddToClassList(BaseClass);
drag = new DragManipulator(this);
drag.Dragged += OnDrag;
this.AddManipulator(drag);
RegisterCallback<NavigationMoveEvent>(OnNavigationMove);
viewModel.Rotated += OnRotated;
}
private void CreateItems()
{
Clear();
if (DisplayCount == 0 || template == null) { return; }
for (int i = 0; i < DisplayCount; i++)
{
var ve = template.CloneTree();
ve.focusable = true;
Add(ve);
}
FocusFirst();
}
public void Rebind()
{
if (viewModel == null || ItemSource == null || DisplayCount == 0) { return; }
for (int i = 0; i < DisplayCount; i++)
{
TryBindItem(this[i], i);
}
}
private bool HasItem(int index)
{
return 0 <= index && index < ItemSource.Count;
}
private void OnRotated(object sender, RecyclerViewModel.RotateEvent e)
{
var first = 0;
var last = DisplayCount - 1;
var (from, to) = e.Direction switch
{
RecyclerViewModel.Direction.Clockwise => (first, last),
RecyclerViewModel.Direction.CounterClockwise => (last, first),
RecyclerViewModel.Direction.None => (-1, -1)
};
if (from == -1 && to == -1) { return; }
var element = this[from];
RemoveAt(from);
Insert(to, element);
element.dataSource = viewModel.GetItem(viewModel.GetOffset(to));
FocusFirst();
NotifySelection();
}
private void NotifySelection()
{
using SelectEvent evt = SelectEvent.GetPooled();
evt.target = this[0];
evt.SelectedIndex = viewModel.SelectedIndex;
evt.SelectedItem = viewModel.GetItem(evt.SelectedIndex);
SendEvent(evt);
}
private void FocusFirst()
{
if (childCount > 0)
{
this[0].Focus();
}
}
private void TryBindItem(VisualElement element, int index)
{
if (HasItem(index))
{
var idx = viewModel.WrapIndex(index);
if (0 <= idx && idx < ItemSource.Count)
{
element.dataSource = ItemSource[idx];
}
}
else
{
element.dataSource = null;
}
}
private void OnDrag(Vector2 vector)
{
throw new NotImplementedException();
}
private void OnNavigationMove(NavigationMoveEvent evt)
{
var direction = evt.direction switch
{
NavigationMoveEvent.Direction.Left => RecyclerViewModel.Direction.CounterClockwise,
NavigationMoveEvent.Direction.Right => RecyclerViewModel.Direction.Clockwise,
NavigationMoveEvent.Direction.Next => RecyclerViewModel.Direction.Clockwise,
NavigationMoveEvent.Direction.Previous => RecyclerViewModel.Direction.CounterClockwise,
_ => RecyclerViewModel.Direction.None
};
viewModel.RotateCommand.TryExecute(direction);
}
}
}