using System; using System.Collections.Generic; using System.Linq; using KitsuneCafe.System.Collections; using R3; using UnityEngine; using UnityEngine.UIElements; namespace KitsuneCafe.UI { interface IRecycleVirtualizationContainer : IVirtualizationController { ILayout Layout { get; set; } } public class RecycleVirtualizationController : IRecycleVirtualizationContainer, IDisposable { internal record ItemGeometry(float Position, float Size); public abstract record UpdateRequest; public record FullUpdate : UpdateRequest; public record PartialUpdate(int Index) : UpdateRequest; private ICollectionDataSource dataSource; public ICollectionDataSource DataSource { get => dataSource; set { if (dataSource != value) { dataSource = value; Setup(); } } } private VisualElement container; public VisualElement Container { get => container; set { if (container != value) { container = value; Setup(); } } } private ILayout layout; public ILayout Layout { get => layout; set { if (layout != value) { layout = value; Setup(); } } } public FlowDirection Direction => Layout.Direction; private readonly Queue itemPool = new(); private readonly BiDictionary visibleItems = new(); private float parentSize = 0; private float lastKnownScrollOffset = 0; private readonly Subject requestSubject = new(); private IDisposable subscriptions; public void Setup() { Dispose(); if (DataSource == null || DataSource.Length == 0 || Layout == null || Container == null) { return; } var full = requestSubject.Where(req => req is FullUpdate) .ThrottleLastFrame(1) .Subscribe(_ => UpdateVisibleItems()); var partial = requestSubject.Where(req => req is PartialUpdate) .Select(req => (PartialUpdate)req) .ChunkFrame(1) .Where(requests => requests.Any()) .Subscribe(requests => { var index = requests.Min(req => req.Index); UpdateFromIndex(index); }); subscriptions = Disposable.Combine(full, partial); Layout.Update(DataSource.Length, Container); ScheduleUpdate(); } public void OnScrolled(float scrollOffset) { lastKnownScrollOffset = scrollOffset; ScheduleUpdate(); } public void OnParentSizeChanged(Vector2 size) { parentSize = Direction switch { FlowDirection.Vertical => size.y, FlowDirection.Horizontal => size.x, _ => throw new NotImplementedException(), }; if (!float.IsNaN(parentSize)) { ScheduleUpdate(); } } private void ScheduleUpdate() { requestSubject.OnNext(new FullUpdate()); } private void ScheduleUpdate(int index) { requestSubject.OnNext(new PartialUpdate(index)); } private void UpdateSize(VisualElement element, StyleLength size) { if (Layout is null) { element.style.width = 0; element.style.height = 0; } else if (Direction == FlowDirection.Vertical) { element.style.width = StyleKeyword.Auto; element.style.height = size; } else if (Direction == FlowDirection.Horizontal) { element.style.height = StyleKeyword.Auto; element.style.width = size; } } private VisualElement RecycleItem() { var item = itemPool.Dequeue(); item.UnregisterCallback(OnItemGeometryChanged); return item; } private VisualElement CreateItem(int index) { var item = DataSource.CreateItem(); if (Layout is FixedLayout) { float itemSize = Layout.GetItemSize(index); UpdateSize(item, itemSize); } return item; } private VisualElement GetItem(int index) { VisualElement item = itemPool.Count > 0 ? RecycleItem() : CreateItem(index); item.RegisterCallback(OnItemGeometryChanged, index); DataSource.BindItem(item, index); visibleItems[index] = item; Container.Add(item); var position = Layout.GetItemPosition(index); if (Direction == FlowDirection.Horizontal) { item.style.left = position; } else { item.style.top = position; } if (Layout is DynamicLayout layout) { var size = Direction switch { FlowDirection.Vertical => item.resolvedStyle.height, FlowDirection.Horizontal => item.resolvedStyle.width, _ => throw new NotImplementedException() }; if (!float.IsNaN(size)) { layout.SetMeasuredItemSize(index, size); } } return item; } private void OnItemGeometryChanged(GeometryChangedEvent evt, int index) { if (Layout is DynamicLayout layout) { var newSize = Direction == FlowDirection.Horizontal ? evt.newRect.width : evt.newRect.height; layout.SetMeasuredItemSize(index, newSize); UpdateSize(Container, Layout.ContentSize); ScheduleUpdate(index); } } private void UpdateVisibleItems() { if (DataSource == null || DataSource.Length == 0 || Mathf.Approximately(parentSize, 0)) { return; } var scrollOffset = lastKnownScrollOffset; var range = layout.GetVisibleRange(scrollOffset, parentSize); var firstIndex = range.Start.Value; var lastIndex = range.End.Value; var itemsToRemove = new List(); foreach (var (index, item) in visibleItems) { if (index < firstIndex || index >= lastIndex) { item.Value.UnregisterCallback(OnItemGeometryChanged); DataSource.UnbindItem(item, index); itemPool.Enqueue(item); Container.Remove(item); itemsToRemove.Add(index); } } foreach (int index in itemsToRemove) { visibleItems.Remove(index); } for (int i = firstIndex; i < lastIndex; i++) { if (!visibleItems.ContainsKey(i)) { var item = GetItem(i); float position = Layout.GetItemPosition(i); if (Direction == FlowDirection.Horizontal) { item.style.left = position; } else { item.style.top = position; } } } } private void UpdateFromIndex(int index) { foreach (var (i, item) in visibleItems) { if (i >= index) { float position = Layout.GetItemPosition(i); if (Direction == FlowDirection.Horizontal) { item.Value.style.left = position; } else { item.Value.style.top = position; } } } } public float GetContentSize() { return Layout.ContentSize; } private void UnregisterCallback(VisualElement element) { element.UnregisterCallback(OnItemGeometryChanged); } public void Dispose() { foreach (var item in visibleItems.Values) { UnregisterCallback(item); } foreach (var item in itemPool) { UnregisterCallback(item); } lastKnownScrollOffset = 0; subscriptions?.Dispose(); Container.Clear(); itemPool.Clear(); visibleItems.Clear(); } ~RecycleVirtualizationController() { Dispose(); } } }