use UniverseLib

This commit is contained in:
Sinai
2021-12-02 18:35:46 +11:00
parent 077a2b434a
commit 3334549902
111 changed files with 540 additions and 7819 deletions

View File

@ -4,10 +4,13 @@ using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.Core.Input;
using UniverseLib.Input;
using UnityExplorer.Core.Runtime;
using UnityExplorer.UI;
using UnityExplorer.UI.Panels;
using UniverseLib.UI.Widgets;
using UniverseLib;
using UniverseLib.UI;
namespace UnityExplorer.UI.Widgets.AutoComplete
{

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UniverseLib.UI;
namespace UnityExplorer.UI.Widgets.AutoComplete
{

View File

@ -1,14 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Core.Input;
using UnityExplorer.Core.Runtime;
using UnityExplorer.UI.Models;
using UnityExplorer.UI.Panels;
using UniverseLib;
using UniverseLib.UI;
namespace UnityExplorer.UI.Widgets.AutoComplete
{

View File

@ -1,136 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using UnityExplorer;
using UnityExplorer.Core;
using UnityExplorer.UI;
using UnityExplorer.UI.Models;
namespace UnityExplorer.UI.Widgets
{
public class AutoSliderScrollbar : UIBehaviourModel
{
public override GameObject UIRoot
{
get
{
if (Slider)
return Slider.gameObject;
return null;
}
}
//public event Action<float> OnValueChanged;
internal readonly Scrollbar Scrollbar;
internal readonly Slider Slider;
internal RectTransform ContentRect;
internal RectTransform ViewportRect;
//internal InputFieldScroller m_parentInputScroller;
public AutoSliderScrollbar(Scrollbar scrollbar, Slider slider, RectTransform contentRect, RectTransform viewportRect)
{
this.Scrollbar = scrollbar;
this.Slider = slider;
this.ContentRect = contentRect;
this.ViewportRect = viewportRect;
this.Scrollbar.onValueChanged.AddListener(this.OnScrollbarValueChanged);
this.Slider.onValueChanged.AddListener(this.OnSliderValueChanged);
//this.RefreshVisibility();
this.Slider.Set(0f, false);
}
private float lastAnchorPosition;
private float lastContentHeight;
private float lastViewportHeight;
private bool _refreshWanted;
public override void Update()
{
if (!Enabled)
return;
_refreshWanted = false;
if (ContentRect.localPosition.y != lastAnchorPosition)
{
lastAnchorPosition = ContentRect.localPosition.y;
_refreshWanted = true;
}
if (ContentRect.rect.height != lastContentHeight)
{
lastContentHeight = ContentRect.rect.height;
_refreshWanted = true;
}
if (ViewportRect.rect.height != lastViewportHeight)
{
lastViewportHeight = ViewportRect.rect.height;
_refreshWanted = true;
}
if (_refreshWanted)
UpdateSliderHandle();
}
public void UpdateSliderHandle()
{
// calculate handle size based on viewport / total data height
var totalHeight = ContentRect.rect.height;
var viewportHeight = ViewportRect.rect.height;
if (totalHeight <= viewportHeight)
{
Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 0f);
Slider.value = 0f;
Slider.interactable = false;
return;
}
var handleHeight = viewportHeight * Math.Min(1, viewportHeight / totalHeight);
handleHeight = Math.Max(15f, handleHeight);
// resize the handle container area for the size of the handle (bigger handle = smaller container)
var container = Slider.m_HandleContainerRect;
container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f));
container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f);
// set handle size
Slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight);
// if slider is 100% height then make it not interactable
Slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight);
float val = 0f;
if (totalHeight > 0f)
val = (float)((decimal)ContentRect.localPosition.y / (decimal)(totalHeight - ViewportRect.rect.height));
Slider.value = val;
}
public void OnScrollbarValueChanged(float value)
{
value = 1f - value;
if (this.Slider.value != value)
this.Slider.Set(value, false);
//OnValueChanged?.Invoke(value);
}
public void OnSliderValueChanged(float value)
{
value = 1f - value;
this.Scrollbar.value = value;
//OnValueChanged?.Invoke(value);
}
public override void ConstructUI(GameObject parent)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,71 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
namespace UnityExplorer.UI.Widgets
{
public class ButtonCell : ICell
{
public float DefaultHeight => 25f;
public Action<int> OnClick;
public int CurrentDataIndex;
public ButtonRef Button;
#region ICell
public bool Enabled => m_enabled;
private bool m_enabled;
public GameObject UIRoot { get; set; }
public RectTransform Rect { get; set; }
public void Disable()
{
m_enabled = false;
UIRoot.SetActive(false);
}
public void Enable()
{
m_enabled = true;
UIRoot.SetActive(true);
}
#endregion
public virtual GameObject CreateContent(GameObject parent)
{
UIRoot = UIFactory.CreateHorizontalGroup(parent, "ButtonCell", true, false, true, true, 2, default,
new Color(0.11f, 0.11f, 0.11f), TextAnchor.MiddleCenter);
Rect = UIRoot.GetComponent<RectTransform>();
Rect.anchorMin = new Vector2(0, 1);
Rect.anchorMax = new Vector2(0, 1);
Rect.pivot = new Vector2(0.5f, 1);
Rect.sizeDelta = new Vector2(25, 25);
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
UIRoot.SetActive(false);
this.Button = UIFactory.CreateButton(UIRoot, "NameButton", "Name");
UIFactory.SetLayoutElement(Button.Component.gameObject, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0);
var buttonText = Button.Component.GetComponentInChildren<Text>();
buttonText.horizontalOverflow = HorizontalWrapMode.Overflow;
buttonText.alignment = TextAnchor.MiddleLeft;
Color normal = new Color(0.11f, 0.11f, 0.11f);
Color highlight = new Color(0.16f, 0.16f, 0.16f);
Color pressed = new Color(0.05f, 0.05f, 0.05f);
Color disabled = new Color(1, 1, 1, 0);
RuntimeProvider.Instance.SetColorBlock(Button.Component, normal, highlight, pressed, disabled);
Button.OnClick += () => { OnClick?.Invoke(CurrentDataIndex); };
return UIRoot;
}
}
}

View File

@ -1,80 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.UI.Widgets
{
public class ButtonListHandler<TData, TCell> : ICellPoolDataSource<TCell> where TCell : ButtonCell
{
internal ScrollPool<TCell> ScrollPool;
public int ItemCount => currentEntries.Count;
public readonly List<TData> currentEntries = new List<TData>();
public Func<List<TData>> GetEntries;
public Action<TCell, int> SetICell;
public Func<TData, string, bool> ShouldDisplay;
public Action<int> OnCellClicked;
public string CurrentFilter
{
get => currentFilter;
set => currentFilter = value ?? "";
}
private string currentFilter;
public ButtonListHandler(ScrollPool<TCell> scrollPool, Func<List<TData>> getEntriesMethod,
Action<TCell, int> setICellMethod, Func<TData, string, bool> shouldDisplayMethod,
Action<int> onCellClickedMethod)
{
ScrollPool = scrollPool;
GetEntries = getEntriesMethod;
SetICell = setICellMethod;
ShouldDisplay = shouldDisplayMethod;
OnCellClicked = onCellClickedMethod;
}
public void RefreshData()
{
var allEntries = GetEntries();
currentEntries.Clear();
foreach (var entry in allEntries)
{
if (!string.IsNullOrEmpty(currentFilter))
{
if (!ShouldDisplay(entry, currentFilter))
continue;
currentEntries.Add(entry);
}
else
currentEntries.Add(entry);
}
}
public virtual void OnCellBorrowed(TCell cell)
{
cell.OnClick += OnCellClicked;
}
public virtual void SetCell(TCell cell, int index)
{
if (currentEntries == null)
RefreshData();
if (index < 0 || index >= currentEntries.Count)
cell.Disable();
else
{
cell.Enable();
cell.CurrentDataIndex = index;
SetICell(cell, index);
}
}
}
}

View File

@ -1,126 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.UI.Models;
namespace UnityExplorer.UI.Widgets
{
// To fix an issue with Input Fields and allow them to go inside a ScrollRect nicely.
public class InputFieldScroller : UIBehaviourModel
{
public override GameObject UIRoot
{
get
{
if (InputField.UIRoot)
return InputField.UIRoot;
return null;
}
}
public Action OnScroll;
internal AutoSliderScrollbar Slider;
internal InputFieldRef InputField;
internal RectTransform ContentRect;
internal RectTransform ViewportRect;
internal static CanvasScaler RootScaler;
public InputFieldScroller(AutoSliderScrollbar sliderScroller, InputFieldRef inputField)
{
this.Slider = sliderScroller;
this.InputField = inputField;
inputField.OnValueChanged += OnTextChanged;
ContentRect = inputField.UIRoot.GetComponent<RectTransform>();
ViewportRect = ContentRect.transform.parent.GetComponent<RectTransform>();
if (!RootScaler)
RootScaler = UIManager.CanvasRoot.GetComponent<CanvasScaler>();
}
internal string m_lastText;
internal bool m_updateWanted;
internal bool m_wantJumpToBottom;
private float m_desiredContentHeight;
private float lastContentPosition;
private float lastViewportHeight;
public override void Update()
{
if (this.ContentRect.localPosition.y != lastContentPosition)
{
lastContentPosition = ContentRect.localPosition.y;
OnScroll?.Invoke();
}
if (ViewportRect.rect.height != lastViewportHeight)
{
lastViewportHeight = ViewportRect.rect.height;
m_updateWanted = true;
}
if (m_updateWanted)
{
m_updateWanted = false;
ProcessInputText();
float desiredHeight = Math.Max(m_desiredContentHeight, ViewportRect.rect.height);
if (ContentRect.rect.height < desiredHeight)
{
ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight);
this.Slider.UpdateSliderHandle();
}
else if (ContentRect.rect.height > desiredHeight)
{
ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight);
this.Slider.UpdateSliderHandle();
}
}
if (m_wantJumpToBottom)
{
Slider.Slider.value = 1f;
m_wantJumpToBottom = false;
}
}
internal void OnTextChanged(string text)
{
m_lastText = text;
m_updateWanted = true;
}
internal void ProcessInputText()
{
var curInputRect = InputField.Component.textComponent.rectTransform.rect;
var scaleFactor = RootScaler.scaleFactor;
// Current text settings
var texGenSettings = InputField.Component.textComponent.GetGenerationSettings(curInputRect.size);
texGenSettings.generateOutOfBounds = false;
texGenSettings.scaleFactor = scaleFactor;
// Preferred text rect height
var textGen = InputField.Component.textComponent.cachedTextGeneratorForLayout;
m_desiredContentHeight = textGen.GetPreferredHeight(m_lastText, texGenSettings) + 10;
}
public override void ConstructUI(GameObject parent)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,279 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.UI.Widgets
{
public class DataHeightCache<T> where T : ICell
{
private ScrollPool<T> ScrollPool { get; }
public DataHeightCache(ScrollPool<T> scrollPool)
{
ScrollPool = scrollPool;
}
private readonly List<DataViewInfo> heightCache = new List<DataViewInfo>();
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;
/// <summary>
/// Lookup table for "which data index first appears at this position"<br/>
/// Index: DefaultHeight * index from top of data<br/>
/// Value: the first data index at this position<br/>
/// </summary>
private readonly List<int> rangeCache = new List<int>();
/// <summary>Same as GetRangeIndexOfPosition, except this rounds up to the next division if there was remainder from the previous cell.</summary>
private int GetRangeCeilingOfPosition(float position) => (int)Math.Ceiling((decimal)position / (decimal)DefaultHeight);
/// <summary>Get the first range (division of DefaultHeight) which the position appears in.</summary>
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;
}
/// <summary>
/// Get the spread of the height, starting from the start position.<br/><br/>
/// The "spread" begins at the start of the next interval of the DefaultHeight, then increases for
/// every interval beyond that.
/// </summary>
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);
}
/// <summary>Append a data index to the cache with the provided height value.</summary>
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;
}
/// <summary>Remove the last (highest count) index from the height cache.</summary>
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);
}
/// <summary>Set a given data index with the specified value.</summary>
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();
}
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityExplorer.UI.Models;
namespace UnityExplorer.UI.Widgets
{
public interface ICell : IPooledObject
{
bool Enabled { get; }
RectTransform Rect { get; set; }
void Enable();
void Disable();
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.UI.Widgets
{
public interface ICellPoolDataSource<T> where T : ICell
{
int ItemCount { get; }
void OnCellBorrowed(T cell);
//void ReleaseCell(T cell);
void SetCell(T cell, int index);
//void DisableCell(T cell, int index);
}
}

View File

@ -1,703 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Core.Input;
using UnityExplorer.UI.Models;
using UnityExplorer.UI.Panels;
namespace UnityExplorer.UI.Widgets
{
public struct CellInfo
{
public int cellIndex, dataIndex;
}
/// <summary>
/// An object-pooled ScrollRect, attempts to support content of any size and provide a scrollbar for it.
/// </summary>
public class ScrollPool<T> : UIBehaviourModel, IEnumerable<CellInfo> where T : ICell
{
public ScrollPool(ScrollRect scrollRect)
{
this.ScrollRect = scrollRect;
}
public ICellPoolDataSource<T> DataSource { get; set; }
public readonly List<T> CellPool = new List<T>();
internal DataHeightCache<T> HeightCache;
public float PrototypeHeight => _protoHeight ?? (float)(_protoHeight = Pool<T>.Instance.DefaultHeight);
private float? _protoHeight;
public int ExtraPoolCells => 10;
public float RecycleThreshold => PrototypeHeight * ExtraPoolCells;
public float HalfThreshold => RecycleThreshold * 0.5f;
// UI
public override GameObject UIRoot
{
get
{
if (ScrollRect)
return ScrollRect.gameObject;
return null;
}
}
public RectTransform Viewport => ScrollRect.viewport;
public RectTransform Content => ScrollRect.content;
internal Slider slider;
internal ScrollRect ScrollRect;
internal VerticalLayoutGroup contentLayout;
// Cache / tracking
private Vector2 RecycleViewBounds;
private Vector2 NormalizedScrollBounds;
/// <summary>
/// The first and last pooled indices relative to the DataSource's list
/// </summary>
private int bottomDataIndex;
private int TopDataIndex => Math.Max(0, bottomDataIndex - CellPool.Count + 1);
private int CurrentDataCount => bottomDataIndex + 1;
private float TotalDataHeight => HeightCache.TotalHeight + contentLayout.padding.top + contentLayout.padding.bottom;
/// <summary>
/// The first and last indices of our CellPool in the transform heirarchy
/// </summary>
private int topPoolIndex, bottomPoolIndex;
private Vector2 prevAnchoredPos;
private float prevViewportHeight;
#region Internal set tracking and update
// A sanity check so only one thing is setting the value per frame.
public bool WritingLocked
{
get => writingLocked || PanelDragger.Resizing;
internal set
{
if (writingLocked == value)
return;
timeofLastWriteLock = Time.realtimeSinceStartup;
writingLocked = value;
}
}
private bool writingLocked;
private float timeofLastWriteLock;
private float prevContentHeight = 1.0f;
private event Action OnHeightChanged;
public override void Update()
{
if (!ScrollRect || DataSource == null)
return;
if (writingLocked && timeofLastWriteLock.OccuredEarlierThanDefault())
writingLocked = false;
if (!writingLocked)
{
bool viewChange = CheckRecycleViewBounds(true);
if (viewChange || Content.rect.height != prevContentHeight)
{
prevContentHeight = Content.rect.height;
OnValueChangedListener(Vector2.zero);
OnHeightChanged?.Invoke();
}
}
}
#endregion
// Public methods
public void Refresh(bool setCellData, bool jumpToTop = false)
{
if (jumpToTop)
{
bottomDataIndex = CellPool.Count - 1;
Content.anchoredPosition = Vector2.zero;
}
RefreshCells(setCellData, true);
}
public void JumpToIndex(int index, Action<T> onJumped)
{
RefreshCells(true, true);
// Slide to the normalized position of the index
var cache = HeightCache[index];
float normalized = (cache.startPosition + (cache.height * 0.5f)) / HeightCache.TotalHeight;
RuntimeProvider.Instance.StartCoroutine(ForceDelayedJump(index, normalized, onJumped));
}
private IEnumerator ForceDelayedJump(int dataIndex, float normalizedPos, Action<T> onJumped)
{
// Yielding two frames seems necessary in case the Explorer tab had not been opened before the jump.
yield return null;
yield return null;
slider.value = normalizedPos;
// Get the cell containing the data index and invoke the onJumped listener for it
foreach (var cellInfo in this)
{
if (cellInfo.dataIndex == dataIndex)
{
onJumped?.Invoke(CellPool[cellInfo.cellIndex]);
break;
}
}
}
// IEnumerable
public IEnumerator<CellInfo> GetEnumerator() => EnumerateCellPool();
IEnumerator IEnumerable.GetEnumerator() => EnumerateCellPool();
// Initialize
/// <summary>Should be called only once, when the scroll pool is created.</summary>
public void Initialize(ICellPoolDataSource<T> dataSource, Action onHeightChangedListener = null)
{
this.DataSource = dataSource;
HeightCache = new DataHeightCache<T>(this);
// Ensure the pool for the cell type is initialized.
Pool<T>.GetPool();
this.contentLayout = ScrollRect.content.GetComponent<VerticalLayoutGroup>();
this.slider = ScrollRect.GetComponentInChildren<Slider>();
slider.onValueChanged.AddListener(OnSliderValueChanged);
ScrollRect.vertical = true;
ScrollRect.horizontal = false;
RuntimeProvider.Instance.StartCoroutine(InitCoroutine(onHeightChangedListener));
}
private IEnumerator InitCoroutine(Action onHeightChangedListener)
{
ScrollRect.content.anchoredPosition = Vector2.zero;
yield return null;
yield return null;
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
// set intial bounds
prevAnchoredPos = Content.anchoredPosition;
CheckRecycleViewBounds(false);
// create initial cell pool and set cells
CreateCellPool();
foreach (var cell in this)
SetCell(CellPool[cell.cellIndex], cell.dataIndex);
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
prevContentHeight = Content.rect.height;
// update slider
SetScrollBounds();
UpdateSliderHandle();
// add onValueChanged listener after setup
ScrollRect.onValueChanged.AddListener(OnValueChangedListener);
OnHeightChanged += onHeightChangedListener;
onHeightChangedListener?.Invoke();
}
private void SetScrollBounds()
{
NormalizedScrollBounds = new Vector2(Viewport.rect.height * 0.5f, TotalDataHeight - (Viewport.rect.height * 0.5f));
}
/// <summary>
/// return value = viewport changed height
/// </summary>
private bool CheckRecycleViewBounds(bool extendPoolIfGrown)
{
RecycleViewBounds = new Vector2(Viewport.MinY() + HalfThreshold, Viewport.MaxY() - HalfThreshold);
if (extendPoolIfGrown && prevViewportHeight < Viewport.rect.height && prevViewportHeight != 0.0f)
CheckExtendCellPool();
bool ret = prevViewportHeight == Viewport.rect.height;
prevViewportHeight = Viewport.rect.height;
return ret;
}
// Cell pool
private CellInfo _current;
private IEnumerator<CellInfo> EnumerateCellPool()
{
int cellIdx = topPoolIndex;
int dataIndex = TopDataIndex;
int iterated = 0;
while (iterated < CellPool.Count)
{
_current.cellIndex = cellIdx;
_current.dataIndex = dataIndex;
yield return _current;
cellIdx++;
if (cellIdx >= CellPool.Count)
cellIdx = 0;
dataIndex++;
iterated++;
}
}
private void CreateCellPool()
{
//ReleaseCells();
CheckDataSourceCountChange(out _);
float currentPoolCoverage = 0f;
float requiredCoverage = ScrollRect.viewport.rect.height + RecycleThreshold;
topPoolIndex = 0;
bottomPoolIndex = -1;
WritingLocked = true;
// create cells until the Pool area is covered.
// use minimum default height so that maximum pool count is reached.
while (currentPoolCoverage <= requiredCoverage)
{
bottomPoolIndex++;
var cell = Pool<T>.Borrow();
CellPool.Add(cell);
DataSource.OnCellBorrowed(cell);
cell.Rect.SetParent(ScrollRect.content, false);
currentPoolCoverage += PrototypeHeight;
}
bottomDataIndex = CellPool.Count - 1;
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
}
private bool CheckExtendCellPool()
{
CheckDataSourceCountChange();
var requiredCoverage = Math.Abs(RecycleViewBounds.y - RecycleViewBounds.x);
var currentCoverage = CellPool.Count * PrototypeHeight;
int cellsRequired = (int)Math.Floor((decimal)(requiredCoverage - currentCoverage) / (decimal)PrototypeHeight);
if (cellsRequired > 0)
{
WritingLocked = true;
bottomDataIndex += cellsRequired;
// TODO sometimes still jumps a litte bit, need to figure out why.
float prevAnchor = Content.localPosition.y;
float prevHeight = Content.rect.height;
for (int i = 0; i < cellsRequired; i++)
{
var cell = Pool<T>.Borrow();
DataSource.OnCellBorrowed(cell);
cell.Rect.SetParent(ScrollRect.content, false);
CellPool.Add(cell);
if (CellPool.Count > 1)
{
int index = CellPool.Count - 1 - (topPoolIndex % (CellPool.Count - 1));
cell.Rect.SetSiblingIndex(index + 1);
if (bottomPoolIndex == index - 1)
bottomPoolIndex++;
}
}
RefreshCells(true, true);
//ExplorerCore.Log("Anchor: " + Content.localPosition.y + ", prev: " + prevAnchor);
//ExplorerCore.Log("Height: " + Content.rect.height + ", prev:" + prevHeight);
if (Content.localPosition.y != prevAnchor)
{
var diff = Content.localPosition.y - prevAnchor;
Content.localPosition = new Vector3(Content.localPosition.x, Content.localPosition.y - diff);
}
if (Content.rect.height != prevHeight)
{
var diff = Content.rect.height - prevHeight;
//ExplorerCore.Log("Height diff: " + diff);
//Content.localPosition = new Vector3(Content.localPosition.x, Content.localPosition.y - diff);
}
return true;
}
return false;
}
// Refresh methods
private bool CheckDataSourceCountChange() => CheckDataSourceCountChange(out _);
private bool CheckDataSourceCountChange(out bool shouldJumpToBottom)
{
shouldJumpToBottom = false;
int count = DataSource.ItemCount;
if (bottomDataIndex > count && bottomDataIndex >= CellPool.Count)
{
bottomDataIndex = Math.Max(count - 1, CellPool.Count - 1);
shouldJumpToBottom = true;
}
if (HeightCache.Count < count)
{
HeightCache.SetIndex(count - 1, PrototypeHeight);
return true;
}
else if (HeightCache.Count > count)
{
while (HeightCache.Count > count)
HeightCache.RemoveLast();
return false;
}
return false;
}
private void RefreshCells(bool andReloadFromDataSource, bool setSlider)
{
if (!CellPool.Any()) return;
CheckRecycleViewBounds(true);
CheckDataSourceCountChange(out bool jumpToBottom);
// update date height cache, and set cells if 'andReload'
foreach (var cellInfo in this)
{
var cell = CellPool[cellInfo.cellIndex];
if (andReloadFromDataSource)
SetCell(cell, cellInfo.dataIndex);
else
HeightCache.SetIndex(cellInfo.dataIndex, cell.Rect.rect.height);
}
// force check recycles
if (andReloadFromDataSource)
{
RecycleBottomToTop();
RecycleTopToBottom();
}
if (setSlider)
UpdateSliderHandle();
if (jumpToBottom)
{
var diff = Viewport.MaxY() - CellPool[bottomPoolIndex].Rect.MaxY();
Content.anchoredPosition += Vector2.up * diff;
}
if (andReloadFromDataSource)
LayoutRebuilder.ForceRebuildLayoutImmediate(Content);
SetScrollBounds();
ScrollRect.UpdatePrevData();
}
private void RefreshCellHeightsFast()
{
foreach (var cellInfo in this)
HeightCache.SetIndex(cellInfo.dataIndex, CellPool[cellInfo.cellIndex].Rect.rect.height);
}
private void SetCell(T cachedCell, int dataIndex)
{
cachedCell.Enable();
DataSource.SetCell(cachedCell, dataIndex);
LayoutRebuilder.ForceRebuildLayoutImmediate(cachedCell.Rect);
HeightCache.SetIndex(dataIndex, cachedCell.Rect.rect.height);
}
// Value change processor
private void OnValueChangedListener(Vector2 val)
{
if (WritingLocked || DataSource == null)
return;
if (InputManager.MouseScrollDelta != Vector2.zero)
ScrollRect.StopMovement();
RefreshCellHeightsFast();
CheckRecycleViewBounds(true);
float yChange = ((Vector2)ScrollRect.content.localPosition - prevAnchoredPos).y;
float adjust = 0f;
if (yChange > 0) // Scrolling down
{
if (ShouldRecycleTop)
adjust = RecycleTopToBottom();
}
else if (yChange < 0) // Scrolling up
{
if (ShouldRecycleBottom)
adjust = RecycleBottomToTop();
}
var vector = new Vector2(0, adjust);
ScrollRect.m_ContentStartPosition += vector;
ScrollRect.m_PrevPosition += vector;
prevAnchoredPos = ScrollRect.content.anchoredPosition;
SetScrollBounds();
UpdateSliderHandle();
}
private bool ShouldRecycleTop => GetCellExtent(CellPool[topPoolIndex].Rect) > RecycleViewBounds.x
&& GetCellExtent(CellPool[bottomPoolIndex].Rect) > RecycleViewBounds.y;
private bool ShouldRecycleBottom => CellPool[bottomPoolIndex].Rect.position.y < RecycleViewBounds.y
&& CellPool[topPoolIndex].Rect.position.y < RecycleViewBounds.x;
private float GetCellExtent(RectTransform cell) => cell.MaxY() - contentLayout.spacing;
private float RecycleTopToBottom()
{
float recycledheight = 0f;
while (ShouldRecycleTop && CurrentDataCount < DataSource.ItemCount)
{
WritingLocked = true;
var cell = CellPool[topPoolIndex];
//Move top cell to bottom
cell.Rect.SetAsLastSibling();
var prevHeight = cell.Rect.rect.height;
// update content position
Content.anchoredPosition -= Vector2.up * prevHeight;
recycledheight += prevHeight + contentLayout.spacing;
//set Cell
SetCell(cell, CurrentDataCount);
//set new indices
bottomDataIndex++;
bottomPoolIndex = topPoolIndex;
topPoolIndex = (topPoolIndex + 1) % CellPool.Count;
}
return -recycledheight;
}
private float RecycleBottomToTop()
{
float recycledheight = 0f;
while (ShouldRecycleBottom && CurrentDataCount > CellPool.Count)
{
WritingLocked = true;
var cell = CellPool[bottomPoolIndex];
//Move bottom cell to top
cell.Rect.SetAsFirstSibling();
var prevHeight = cell.Rect.rect.height;
// update content position
Content.anchoredPosition += Vector2.up * prevHeight;
recycledheight += prevHeight + contentLayout.spacing;
//set new index
bottomDataIndex--;
//set Cell
SetCell(cell, TopDataIndex);
// move content again for new cell size
var newHeight = cell.Rect.rect.height;
var diff = newHeight - prevHeight;
if (diff != 0.0f)
{
Content.anchoredPosition += Vector2.up * diff;
recycledheight += diff;
}
//set new indices
topPoolIndex = bottomPoolIndex;
bottomPoolIndex = (bottomPoolIndex - 1 + CellPool.Count) % CellPool.Count;
}
return recycledheight;
}
// Slider
private void OnSliderValueChanged(float val)
{
// Prevent spam invokes unless value is 0 or 1 (so we dont skip over the start/end)
if (DataSource == null || (WritingLocked && val != 0 && val != 1))
return;
//this.WritingLocked = true;
ScrollRect.StopMovement();
RefreshCellHeightsFast();
// normalize the scroll position for the scroll bounds.
// point at the center of the viewport
var desiredPosition = val * (NormalizedScrollBounds.y - NormalizedScrollBounds.x) + NormalizedScrollBounds.x;
// add offset above it for viewport height
var halfView = Viewport.rect.height * 0.5f;
var desiredMinY = desiredPosition - halfView;
// get the data index at the top of the viewport
int topViewportIndex = HeightCache.GetFirstDataIndexAtPosition(desiredMinY);
topViewportIndex = Math.Max(0, topViewportIndex);
topViewportIndex = Math.Min(DataSource.ItemCount - 1, topViewportIndex);
// get the real top pooled data index to display our content
int poolStartIndex = Math.Max(0, topViewportIndex - (int)(ExtraPoolCells * 0.5f));
poolStartIndex = Math.Min(Math.Max(0, DataSource.ItemCount - CellPool.Count), poolStartIndex);
var topStartPos = HeightCache[poolStartIndex].startPosition;
float desiredAnchor;
if (desiredMinY < HalfThreshold)
desiredAnchor = desiredMinY;
else
desiredAnchor = desiredMinY - topStartPos;
Content.anchoredPosition = new Vector2(0, desiredAnchor);
int desiredBottomIndex = poolStartIndex + CellPool.Count - 1;
// check if our pool indices contain the desired index. If so, rotate and set
if (bottomDataIndex == desiredBottomIndex)
{
// cells will be the same, do nothing
}
else if (TopDataIndex > poolStartIndex && TopDataIndex < desiredBottomIndex)
{
// top cell falls within the new range, rotate around that
int rotate = TopDataIndex - poolStartIndex;
for (int i = 0; i < rotate; i++)
{
CellPool[bottomPoolIndex].Rect.SetAsFirstSibling();
//set new indices
topPoolIndex = bottomPoolIndex;
bottomPoolIndex = (bottomPoolIndex - 1 + CellPool.Count) % CellPool.Count;
bottomDataIndex--;
SetCell(CellPool[topPoolIndex], TopDataIndex);
}
}
else if (bottomDataIndex > poolStartIndex && bottomDataIndex < desiredBottomIndex)
{
// bottom cells falls within the new range, rotate around that
int rotate = desiredBottomIndex - bottomDataIndex;
for (int i = 0; i < rotate; i++)
{
CellPool[topPoolIndex].Rect.SetAsLastSibling();
//set new indices
bottomPoolIndex = topPoolIndex;
topPoolIndex = (topPoolIndex + 1) % CellPool.Count;
bottomDataIndex++;
SetCell(CellPool[bottomPoolIndex], bottomDataIndex);
}
}
else
{
bottomDataIndex = desiredBottomIndex;
foreach (var info in this)
{
var cell = CellPool[info.cellIndex];
SetCell(cell, info.dataIndex);
}
}
CheckRecycleViewBounds(true);
SetScrollBounds();
ScrollRect.UpdatePrevData();
UpdateSliderHandle();
}
private void UpdateSliderHandle()// bool forcePositionValue = true)
{
CheckDataSourceCountChange(out _);
var dataHeight = TotalDataHeight;
// calculate handle size based on viewport / total data height
var viewportHeight = Viewport.rect.height;
var handleHeight = viewportHeight * Math.Min(1, viewportHeight / dataHeight);
handleHeight = Math.Max(15f, handleHeight);
// resize the handle container area for the size of the handle (bigger handle = smaller container)
var container = slider.m_HandleContainerRect;
container.offsetMax = new Vector2(container.offsetMax.x, -(handleHeight * 0.5f));
container.offsetMin = new Vector2(container.offsetMin.x, handleHeight * 0.5f);
// set handle size
slider.handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, handleHeight);
// if slider is 100% height then make it not interactable
slider.interactable = !Mathf.Approximately(handleHeight, viewportHeight);
float val = 0f;
if (TotalDataHeight > 0f)
{
float topPos = 0f;
if (HeightCache.Count > 0)
topPos = HeightCache[TopDataIndex].startPosition;
var scrollPos = topPos + Content.anchoredPosition.y;
var viewHeight = TotalDataHeight - Viewport.rect.height;
if (viewHeight != 0.0f)
val = (float)((decimal)scrollPos / (decimal)(viewHeight));
else
val = 0f;
}
slider.Set(val, false);
}
/// <summary>Use <see cref="UIFactory.CreateScrollPool"/></summary>
public override void ConstructUI(GameObject parent) => throw new NotImplementedException();
}
}

View File

@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.UI
{
public static class UIExtension
{
public static void GetCorners(this RectTransform rect, Vector3[] corners)
{
Vector3 bottomLeft = new Vector3(rect.position.x, rect.position.y - rect.rect.height, 0);
corners[0] = bottomLeft;
corners[1] = bottomLeft + new Vector3(0, rect.rect.height, 0);
corners[2] = bottomLeft + new Vector3(rect.rect.width, rect.rect.height, 0);
corners[3] = bottomLeft + new Vector3(rect.rect.width, 0, 0);
}
// again, using position and rect instead of
public static float MaxY(this RectTransform rect) => rect.position.y - rect.rect.height;
public static float MinY(this RectTransform rect) => rect.position.y;
public static float MaxX(this RectTransform rect) => rect.position.x + rect.rect.width;
public static float MinX(this RectTransform rect) => rect.position.x;
}
}

View File

@ -6,6 +6,9 @@ using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.Inspectors;
using UnityExplorer.UI.Widgets;
using UniverseLib;
using UniverseLib.UI;
using UniverseLib.UI.Widgets;
namespace UnityExplorer.UI.Widgets
{

View File

@ -6,6 +6,8 @@ using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UniverseLib;
using UniverseLib.UI.Widgets;
namespace UnityExplorer.UI.Widgets
{