using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace UnityExplorer.UI.Widgets { public class DataHeightCache where T : ICell { private ScrollPool ScrollPool { get; } public DataHeightCache(ScrollPool scrollPool) { ScrollPool = scrollPool; } private readonly List heightCache = new List(); public DataViewInfo this[int index] { get => heightCache[index]; set => SetIndex(index, value); } public int Count => heightCache.Count; public float TotalHeight => totalHeight; private float totalHeight; public float DefaultHeight => m_defaultHeight ?? (float)(m_defaultHeight = ScrollPool.PrototypeHeight); private float? m_defaultHeight; /// /// Lookup table for "which data index first appears at this position"
/// Index: DefaultHeight * index from top of data
/// Value: the first data index at this position
///
private readonly List rangeCache = new List(); /// Same as GetRangeIndexOfPosition, except this rounds up to the next division if there was remainder from the previous cell. private int GetRangeCeilingOfPosition(float position) => (int)Math.Ceiling((decimal)position / (decimal)DefaultHeight); /// Get the first range (division of DefaultHeight) which the position appears in. private int GetRangeFloorOfPosition(float position) => (int)Math.Floor((decimal)position / (decimal)DefaultHeight); public int GetFirstDataIndexAtPosition(float desiredHeight) { if (!heightCache.Any()) return 0; int rangeIndex = GetRangeFloorOfPosition(desiredHeight); // probably shouldnt happen but just in case if (rangeIndex < 0) return 0; if (rangeIndex >= rangeCache.Count) { int idx = ScrollPool.DataSource.ItemCount - 1; return idx; } int dataIndex = rangeCache[rangeIndex]; var cache = heightCache[dataIndex]; // if the DataViewInfo is outdated, need to rebuild int expectedMin = GetRangeCeilingOfPosition(cache.startPosition); int expectedMax = expectedMin + cache.normalizedSpread - 1; if (rangeIndex < expectedMin || rangeIndex > expectedMax) { RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1); rangeIndex = GetRangeFloorOfPosition(desiredHeight); dataIndex = rangeCache[rangeIndex]; } return dataIndex; } /// /// Get the spread of the height, starting from the start position.

/// The "spread" begins at the start of the next interval of the DefaultHeight, then increases for /// every interval beyond that. ///
private int GetRangeSpread(float startPosition, float height) { // get the remainder of the start position divided by min height float rem = startPosition % DefaultHeight; // if there is a remainder, this means the previous cell started in our first cell and // they take priority, so reduce our height by (minHeight - remainder) to account for that. // We need to fill that gap and reach the next cell before we take priority. if (rem != 0.0f) height -= (DefaultHeight - rem); return (int)Math.Ceiling((decimal)height / (decimal)DefaultHeight); } /// Append a data index to the cache with the provided height value. public void Add(float value) { value = Math.Max(DefaultHeight, value); int spread = GetRangeSpread(totalHeight, value); heightCache.Add(new DataViewInfo(heightCache.Count, value, totalHeight, spread)); int dataIdx = heightCache.Count - 1; for (int i = 0; i < spread; i++) rangeCache.Add(dataIdx); totalHeight += value; } /// Remove the last (highest count) index from the height cache. public void RemoveLast() { if (!heightCache.Any()) return; totalHeight -= heightCache[heightCache.Count - 1]; heightCache.RemoveAt(heightCache.Count - 1); int idx = heightCache.Count; while (rangeCache.Count > 0 && rangeCache[rangeCache.Count - 1] == idx) rangeCache.RemoveAt(rangeCache.Count - 1); } /// Set a given data index with the specified value. public void SetIndex(int dataIndex, float height) { height = (float)Math.Floor(height); height = Math.Max(DefaultHeight, height); // If the index being set is beyond the DataSource item count, prune and return. if (dataIndex >= ScrollPool.DataSource.ItemCount) { while (heightCache.Count > dataIndex) RemoveLast(); return; } // If the data index exceeds our cache count, fill the gap. // This is done by the ScrollPool when the DataSource sets its initial count, or the count increases. if (dataIndex >= heightCache.Count) { while (dataIndex > heightCache.Count) Add(DefaultHeight); Add(height); return; } // We are actually updating an index. First, update the height and the totalHeight. var cache = heightCache[dataIndex]; if (cache.height != height) { var diff = height - cache.height; totalHeight += diff; cache.height = height; } // update our start position using the previous cell (if it exists) if (dataIndex > 0) { var prev = heightCache[dataIndex - 1]; cache.startPosition = prev.startPosition + prev.height; } // Get the normalized range index (actually ceiling) and spread based on our start position and height int rangeIndex = GetRangeCeilingOfPosition(cache.startPosition); int spread = GetRangeSpread(cache.startPosition, height); // If the previous item in the range cache is not the previous data index, there is a gap. if (rangeCache[rangeIndex] != dataIndex) { // Recalculate start positions up to this index. The gap could be anywhere before here. RecalculateStartPositions(ScrollPool.DataSource.ItemCount - 1); // Get the range index and spread again after rebuilding rangeIndex = GetRangeCeilingOfPosition(cache.startPosition); spread = GetRangeSpread(cache.startPosition, height); } if (rangeCache[rangeIndex] != dataIndex) throw new IndexOutOfRangeException($"Trying to set dataIndex {dataIndex} at rangeIndex {rangeIndex}, but cache is corrupt or invalid!"); if (spread != cache.normalizedSpread) { int spreadDiff = spread - cache.normalizedSpread; cache.normalizedSpread = spread; UpdateSpread(dataIndex, rangeIndex, spreadDiff); } // set the struct back to the array heightCache[dataIndex] = cache; } private void UpdateSpread(int dataIndex, int rangeIndex, int spreadDiff) { if (spreadDiff > 0) { while (rangeCache[rangeIndex] == dataIndex && spreadDiff > 0) { rangeCache.Insert(rangeIndex, dataIndex); spreadDiff--; } } else { while (rangeCache[rangeIndex] == dataIndex && spreadDiff < 0) { rangeCache.RemoveAt(rangeIndex); spreadDiff++; } } } private void RecalculateStartPositions(int toIndex) { if (heightCache.Count <= 1) return; rangeCache.Clear(); DataViewInfo cache; DataViewInfo prev = DataViewInfo.None; for (int idx = 0; idx <= toIndex && idx < heightCache.Count; idx++) { cache = heightCache[idx]; if (!prev.Equals(DataViewInfo.None)) cache.startPosition = prev.startPosition + prev.height; else cache.startPosition = 0; cache.normalizedSpread = GetRangeSpread(cache.startPosition, cache.height); for (int i = 0; i < cache.normalizedSpread; i++) rangeCache.Add(cache.dataIndex); heightCache[idx] = cache; prev = cache; } } public struct DataViewInfo { // static public static DataViewInfo None => s_default; private static DataViewInfo s_default = default; public static implicit operator float(DataViewInfo it) => it.height; public DataViewInfo(int index, float height, float startPos, int spread) { this.dataIndex = index; this.height = height; this.startPosition = startPos; this.normalizedSpread = spread; } // instance public int dataIndex, normalizedSpread; public float height, startPosition; public override bool Equals(object obj) { var other = (DataViewInfo)obj; return this.dataIndex == other.dataIndex && this.height == other.height && this.startPosition == other.startPosition && this.normalizedSpread == other.normalizedSpread; } public override int GetHashCode() => base.GetHashCode(); } } }