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 indices; public IReadOnlyList Indices => indices; public readonly ICommand RotateCommand; public event EventHandler Rotated = delegate { }; public RecyclerViewModel() { indices = new(); RotateCommand = new RelayCommand(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 { 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(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); } } }