338 lines
7.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|