276 lines
6.2 KiB
C#
276 lines
6.2 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);
|
|
|
|
public enum Direction
|
|
{
|
|
None = 0,
|
|
Clockwise = -1,
|
|
CounterClockwise = 1
|
|
}
|
|
|
|
private int displayCount;
|
|
public int DisplayCount
|
|
{
|
|
get => displayCount;
|
|
set
|
|
{
|
|
if (displayCount != value)
|
|
{
|
|
displayCount = value;
|
|
CreateIndices();
|
|
}
|
|
}
|
|
}
|
|
|
|
private IList itemSource;
|
|
public IList ItemSource
|
|
{
|
|
get => itemSource;
|
|
set
|
|
{
|
|
if (itemSource != value)
|
|
{
|
|
itemSource = value;
|
|
CreateIndices();
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Count => Math.Min(DisplayCount, ItemSource.Count);
|
|
|
|
private List<int> indices;
|
|
public IReadOnlyList<int> Indices => indices;
|
|
|
|
public readonly ICommand<Direction> RotateCommand;
|
|
|
|
public event EventHandler<RotateEvent> Rotated = delegate { };
|
|
|
|
public RecyclerViewModel()
|
|
{
|
|
indices = new();
|
|
RotateCommand = new RelayCommand<Direction>(CanRotate, Rotate);
|
|
}
|
|
|
|
private void CreateIndices()
|
|
{
|
|
if (itemSource == null || displayCount == 0) { return; }
|
|
indices = Enumerable.Range(0, Math.Min(DisplayCount, ItemSource.Count)).ToList();
|
|
}
|
|
|
|
private bool CanRotate(Direction direction)
|
|
{
|
|
return direction == Direction.Clockwise || direction == Direction.CounterClockwise;
|
|
}
|
|
|
|
public void Rotate(Direction direction)
|
|
{
|
|
if (direction == Direction.Clockwise)
|
|
{
|
|
indices.RemoveAt(0);
|
|
var next = indices.Last() + 1;
|
|
next %= ItemSource.Count;
|
|
indices.Add(next);
|
|
Notify(direction);
|
|
}
|
|
else if (direction == Direction.CounterClockwise)
|
|
{
|
|
indices.RemoveAt(ItemSource.Count - 1);
|
|
var count = ItemSource.Count;
|
|
var previous = indices[0] - 1;
|
|
previous += count % count;
|
|
indices.Insert(0, previous);
|
|
Notify(direction);
|
|
}
|
|
}
|
|
|
|
private void Notify(Direction direction)
|
|
{
|
|
Rotated?.Invoke(this, new RotateEvent(direction, indices.First()));
|
|
}
|
|
}
|
|
|
|
public class SelectEvent : EventBase<SelectEvent>
|
|
{
|
|
public int ElementIndex { get; set; }
|
|
public int DataIndex { get; set; }
|
|
}
|
|
|
|
[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 IBinder binder;
|
|
|
|
[CreateProperty]
|
|
public IBinder Binder
|
|
{
|
|
get => binder;
|
|
set
|
|
{
|
|
if (binder != value)
|
|
{
|
|
binder = 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 || binder == null) { return; }
|
|
for (int i = 0; i < DisplayCount; i++)
|
|
{
|
|
Add(binder.CreateItem());
|
|
}
|
|
|
|
FocusFirst();
|
|
}
|
|
|
|
private void Rebind()
|
|
{
|
|
if (viewModel == null || ItemSource == null || DisplayCount == 0 || binder == null) { 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)
|
|
{
|
|
VisualElement recycled;
|
|
switch (e.Direction)
|
|
{
|
|
case RecyclerViewModel.Direction.Clockwise:
|
|
recycled = this[0];
|
|
RemoveAt(0);
|
|
Insert(DisplayCount - 1, recycled);
|
|
TryBindItem(recycled, DisplayCount - 1);
|
|
NotifySelection();
|
|
break;
|
|
case RecyclerViewModel.Direction.CounterClockwise:
|
|
recycled = this[DisplayCount - 1];
|
|
RemoveAt(DisplayCount - 1);
|
|
Insert(0, recycled);
|
|
TryBindItem(recycled, 0);
|
|
NotifySelection();
|
|
break;
|
|
}
|
|
|
|
FocusFirst();
|
|
}
|
|
|
|
private void NotifySelection()
|
|
{
|
|
using SelectEvent evt = SelectEvent.GetPooled();
|
|
evt.target = this[0];
|
|
evt.ElementIndex = 0;
|
|
evt.DataIndex = viewModel.Indices[0];
|
|
SendEvent(evt);
|
|
}
|
|
|
|
private void FocusFirst()
|
|
{
|
|
if (childCount > 0)
|
|
{
|
|
this[0].Focus();
|
|
}
|
|
}
|
|
|
|
private void TryBindItem(VisualElement element, int index)
|
|
{
|
|
if (HasItem(index))
|
|
{
|
|
var idx = viewModel.Indices[index];
|
|
if (0 <= idx && idx < ItemSource.Count)
|
|
{
|
|
binder.BindItem(element, idx);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
binder.UnbindItem(element);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|