using System; using Unity.Properties; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace KitsuneCafe.UI { public enum FlowDirection { Vertical = 0, Horizontal = 1 } public abstract record CollectionItemSize; [Serializable] public record FixedItemSize(float Size) : CollectionItemSize; [Serializable] public record DynamicItemSize() : CollectionItemSize { public static readonly DynamicItemSize Instance = new(); } public class CollectionItemSizeConverter : UxmlAttributeConverter { public override CollectionItemSize FromString(string value) { return value switch { "()" => new DynamicItemSize(), var v => new FixedItemSize(int.Parse(v)) }; } public override string ToString(CollectionItemSize value) { return value switch { DynamicItemSize => "()", var v => v.ToString(), }; } } [UxmlElement] public partial class RecycleView : VisualElement, IDisposable { public const string RecycleViewBaseClass = "kitsunecafe__recycle-view"; public const string RecycleViewScrollViewClass = "kitsunecafe__recycle-view--scroll-view"; public const string RecycleViewContentContainerClass = "kitsunecafe__recycle-view--content-container"; public const string RecycleViewDynamicItemClass = "kitsunecafe__recycle-view--dynamic-item"; private FlowDirection direction; [UxmlAttribute, CreateProperty] public FlowDirection Direction { get => direction; set { if (direction != value) { direction = value; virtualizationController.Layout = CreateLayout(); UpdateDirection(); } } } private bool isDynamicSize = false; [UxmlAttribute, CreateProperty] public bool IsDynamicSize { get => isDynamicSize; set { if (isDynamicSize != value) { isDynamicSize = value; virtualizationController.Layout = CreateLayout(); } } } private float itemSize = 22; [UxmlAttribute, CreateProperty, Delayed, Tooltip("In a dynamic layout, this is used for initial calculations")] public float ItemSize { get => itemSize; set { if (itemSize != value) { itemSize = value; if (virtualizationController.Layout is FixedLayout fixedLayout) { fixedLayout.ItemSize = value; } else if (virtualizationController.Layout is DynamicLayout dynamicLayout) { dynamicLayout.DefaultItemSize = value; } } } } private int bufferCount = 8; [UxmlAttribute, CreateProperty, Delayed] public int BufferCount { get => bufferCount; set { if (bufferCount != value) { bufferCount = value; if (virtualizationController.Layout is FixedLayout fixedLayout) { fixedLayout.Buffer = value; } else if (virtualizationController.Layout is DynamicLayout dynamicLayout) { dynamicLayout.Buffer = value; } } } } private float gutter = 0; [UxmlAttribute, CreateProperty, Delayed] public float Gutter { get => gutter; set { if (gutter != value) { gutter = value; if (virtualizationController.Layout is FixedLayout fixedLayout) { fixedLayout.GutterSize = value; } else if (virtualizationController.Layout is DynamicLayout dynamicLayout) { dynamicLayout.GutterSize = value; } } } } [CreateProperty] public ICollectionDataSource DataSource { get => virtualizationController?.DataSource; set { if (virtualizationController != null) { virtualizationController.DataSource = value; virtualizationController.Setup(); } } } private readonly ScrollView scrollView; public override VisualElement contentContainer => scrollView.contentContainer; private readonly IRecycleVirtualizationContainer virtualizationController; public RecycleView() { AddToClassList(RecycleViewBaseClass); scrollView = new ScrollView(); scrollView.AddToClassList(RecycleViewScrollViewClass); hierarchy.Add(scrollView); contentContainer.AddToClassList(RecycleViewContentContainerClass); virtualizationController = new RecycleVirtualizationController { Container = contentContainer }; RegisterCallback(OnGeometryChanged); } private ILayout CreateLayout() { return isDynamicSize switch { true => new DynamicLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter }, false => new FixedLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter } }; } private void UpdateDirection() { switch (direction) { case FlowDirection.Vertical: scrollView.mode = ScrollViewMode.Vertical; scrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; scrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden; scrollView.horizontalScroller.valueChanged -= virtualizationController.OnScrolled; scrollView.verticalScroller.valueChanged += virtualizationController.OnScrolled; break; case FlowDirection.Horizontal: scrollView.mode = ScrollViewMode.Horizontal; scrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden; scrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; scrollView.verticalScroller.valueChanged -= virtualizationController.OnScrolled; scrollView.horizontalScroller.valueChanged += virtualizationController.OnScrolled; break; } } public void OnGeometryChanged(GeometryChangedEvent evt) { if (evt.newRect.size != evt.oldRect.size && virtualizationController != null) { virtualizationController.OnParentSizeChanged(evt.newRect.size); } } public void Dispose() { if (virtualizationController is IDisposable disposable) { disposable.Dispose(); } } ~RecycleView() { Dispose(); } } }