DebugLogRecycledListView.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEngine.UI;
  4. // Handles the log items in an optimized way such that existing log items are
  5. // recycled within the list instead of creating a new log item at each chance
  6. namespace IngameDebugConsole
  7. {
  8. public class DebugLogRecycledListView : MonoBehaviour
  9. {
  10. #pragma warning disable 0649
  11. // Cached components
  12. [SerializeField]
  13. private RectTransform transformComponent;
  14. [SerializeField]
  15. private RectTransform viewportTransform;
  16. [SerializeField]
  17. private Color logItemNormalColor1;
  18. [SerializeField]
  19. private Color logItemNormalColor2;
  20. [SerializeField]
  21. private Color logItemSelectedColor;
  22. #pragma warning restore 0649
  23. internal DebugLogManager manager;
  24. private ScrollRect scrollView;
  25. private float logItemHeight;
  26. private DynamicCircularBuffer<DebugLogEntry> entriesToShow = null;
  27. private DynamicCircularBuffer<DebugLogEntryTimestamp> timestampsOfEntriesToShow = null;
  28. private DebugLogEntry selectedLogEntry;
  29. private int indexOfSelectedLogEntry = int.MaxValue;
  30. private float heightOfSelectedLogEntry;
  31. private float DeltaHeightOfSelectedLogEntry { get { return heightOfSelectedLogEntry - logItemHeight; } }
  32. /// These properties are used by <see cref="OnBeforeFilterLogs"/> and <see cref="OnAfterFilterLogs"/>.
  33. private int collapsedOrderOfSelectedLogEntry;
  34. private float scrollDistanceToSelectedLogEntry;
  35. // Log items used to visualize the visible debug entries
  36. private readonly DynamicCircularBuffer<DebugLogItem> visibleLogItems = new DynamicCircularBuffer<DebugLogItem>( 32 );
  37. private bool isCollapseOn = false;
  38. // Current indices of debug entries shown on screen
  39. private int currentTopIndex = -1, currentBottomIndex = -1;
  40. private System.Predicate<DebugLogItem> shouldRemoveLogItemPredicate;
  41. private System.Action<DebugLogItem> poolLogItemAction;
  42. public float ItemHeight { get { return logItemHeight; } }
  43. public float SelectedItemHeight { get { return heightOfSelectedLogEntry; } }
  44. private void Awake()
  45. {
  46. scrollView = viewportTransform.GetComponentInParent<ScrollRect>();
  47. scrollView.onValueChanged.AddListener( ( pos ) =>
  48. {
  49. if( manager.IsLogWindowVisible )
  50. UpdateItemsInTheList( false );
  51. } );
  52. }
  53. public void Initialize( DebugLogManager manager, DynamicCircularBuffer<DebugLogEntry> entriesToShow, DynamicCircularBuffer<DebugLogEntryTimestamp> timestampsOfEntriesToShow, float logItemHeight )
  54. {
  55. this.manager = manager;
  56. this.entriesToShow = entriesToShow;
  57. this.timestampsOfEntriesToShow = timestampsOfEntriesToShow;
  58. this.logItemHeight = logItemHeight;
  59. shouldRemoveLogItemPredicate = ShouldRemoveLogItem;
  60. poolLogItemAction = manager.PoolLogItem;
  61. }
  62. public void SetCollapseMode( bool collapse )
  63. {
  64. isCollapseOn = collapse;
  65. }
  66. // A log item is clicked, highlight it
  67. public void OnLogItemClicked( DebugLogItem item )
  68. {
  69. OnLogItemClickedInternal( item.Index, item );
  70. }
  71. // Force expand the log item at specified index
  72. public void SelectAndFocusOnLogItemAtIndex( int itemIndex )
  73. {
  74. if( indexOfSelectedLogEntry != itemIndex ) // Make sure that we aren't deselecting the target log item
  75. OnLogItemClickedInternal( itemIndex );
  76. float viewportHeight = viewportTransform.rect.height;
  77. float transformComponentCenterYAtTop = viewportHeight * 0.5f;
  78. float transformComponentCenterYAtBottom = transformComponent.sizeDelta.y - viewportHeight * 0.5f;
  79. float transformComponentTargetCenterY = itemIndex * logItemHeight + viewportHeight * 0.5f;
  80. if( transformComponentCenterYAtTop == transformComponentCenterYAtBottom )
  81. scrollView.verticalNormalizedPosition = 0.5f;
  82. else
  83. scrollView.verticalNormalizedPosition = Mathf.Clamp01( Mathf.InverseLerp( transformComponentCenterYAtBottom, transformComponentCenterYAtTop, transformComponentTargetCenterY ) );
  84. manager.SnapToBottom = false;
  85. }
  86. private void OnLogItemClickedInternal( int itemIndex, DebugLogItem referenceItem = null )
  87. {
  88. int indexOfPreviouslySelectedLogEntry = indexOfSelectedLogEntry;
  89. DeselectSelectedLogItem();
  90. if( indexOfPreviouslySelectedLogEntry != itemIndex )
  91. {
  92. selectedLogEntry = entriesToShow[itemIndex];
  93. indexOfSelectedLogEntry = itemIndex;
  94. CalculateSelectedLogEntryHeight( referenceItem );
  95. manager.SnapToBottom = false;
  96. }
  97. CalculateContentHeight();
  98. UpdateItemsInTheList( true );
  99. manager.ValidateScrollPosition();
  100. }
  101. // Deselect the currently selected log item
  102. public void DeselectSelectedLogItem()
  103. {
  104. selectedLogEntry = null;
  105. indexOfSelectedLogEntry = int.MaxValue;
  106. heightOfSelectedLogEntry = 0f;
  107. }
  108. /// <summary>
  109. /// Cache the currently selected log item's properties so that its position can be restored after <see cref="OnAfterFilterLogs"/> is called.
  110. /// </summary>
  111. public void OnBeforeFilterLogs()
  112. {
  113. collapsedOrderOfSelectedLogEntry = 0;
  114. scrollDistanceToSelectedLogEntry = 0f;
  115. if( selectedLogEntry != null )
  116. {
  117. if( !isCollapseOn )
  118. {
  119. for( int i = 0; i < indexOfSelectedLogEntry; i++ )
  120. {
  121. if( entriesToShow[i] == selectedLogEntry )
  122. collapsedOrderOfSelectedLogEntry++;
  123. }
  124. }
  125. scrollDistanceToSelectedLogEntry = indexOfSelectedLogEntry * ItemHeight - transformComponent.anchoredPosition.y;
  126. }
  127. }
  128. /// <summary>
  129. /// See <see cref="OnBeforeFilterLogs"/>.
  130. /// </summary>
  131. public void OnAfterFilterLogs()
  132. {
  133. // Refresh selected log entry's index
  134. int newIndexOfSelectedLogEntry = -1;
  135. if( selectedLogEntry != null )
  136. {
  137. for( int i = 0; i < entriesToShow.Count; i++ )
  138. {
  139. if( entriesToShow[i] == selectedLogEntry && collapsedOrderOfSelectedLogEntry-- == 0 )
  140. {
  141. newIndexOfSelectedLogEntry = i;
  142. break;
  143. }
  144. }
  145. }
  146. if( newIndexOfSelectedLogEntry < 0 )
  147. DeselectSelectedLogItem();
  148. else
  149. {
  150. indexOfSelectedLogEntry = newIndexOfSelectedLogEntry;
  151. transformComponent.anchoredPosition = new Vector2( 0f, newIndexOfSelectedLogEntry * ItemHeight - scrollDistanceToSelectedLogEntry );
  152. }
  153. }
  154. // Number of debug entries may have changed, update the list
  155. public void OnLogEntriesUpdated( bool updateAllVisibleItemContents )
  156. {
  157. CalculateContentHeight();
  158. UpdateItemsInTheList( updateAllVisibleItemContents );
  159. }
  160. // A single collapsed log entry at specified index is updated, refresh its item if visible
  161. public void OnCollapsedLogEntryAtIndexUpdated( int index )
  162. {
  163. if( index >= currentTopIndex && index <= currentBottomIndex )
  164. {
  165. DebugLogItem logItem = GetLogItemAtIndex( index );
  166. logItem.ShowCount();
  167. if( timestampsOfEntriesToShow != null )
  168. logItem.UpdateTimestamp( timestampsOfEntriesToShow[index] );
  169. }
  170. }
  171. public void RefreshCollapsedLogEntryCounts()
  172. {
  173. for( int i = 0; i < visibleLogItems.Count; i++ )
  174. visibleLogItems[i].ShowCount();
  175. }
  176. public void OnLogEntriesRemoved( int removedLogCount )
  177. {
  178. if( selectedLogEntry != null )
  179. {
  180. bool isSelectedLogEntryRemoved = isCollapseOn ? ( selectedLogEntry.count == 0 ) : ( indexOfSelectedLogEntry < removedLogCount );
  181. if( isSelectedLogEntryRemoved )
  182. DeselectSelectedLogItem();
  183. else
  184. indexOfSelectedLogEntry = isCollapseOn ? FindIndexOfLogEntryInReverseDirection( selectedLogEntry, indexOfSelectedLogEntry ) : ( indexOfSelectedLogEntry - removedLogCount );
  185. }
  186. if( !manager.IsLogWindowVisible && manager.SnapToBottom )
  187. {
  188. // When log window becomes visible, it refreshes all logs. So unless snap to bottom is disabled, we don't need to
  189. // keep track of either the scroll position or the visible log items' positions.
  190. visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );
  191. }
  192. else if( !isCollapseOn )
  193. visibleLogItems.TrimStart( Mathf.Clamp( removedLogCount - currentTopIndex, 0, visibleLogItems.Count ), poolLogItemAction );
  194. else
  195. {
  196. visibleLogItems.RemoveAll( shouldRemoveLogItemPredicate );
  197. if( visibleLogItems.Count > 0 )
  198. removedLogCount = currentTopIndex - FindIndexOfLogEntryInReverseDirection( visibleLogItems[0].Entry, visibleLogItems[0].Index );
  199. }
  200. if( visibleLogItems.Count == 0 )
  201. {
  202. currentTopIndex = -1;
  203. if( !manager.SnapToBottom )
  204. transformComponent.anchoredPosition = Vector2.zero;
  205. }
  206. else
  207. {
  208. currentTopIndex = Mathf.Max( 0, currentTopIndex - removedLogCount );
  209. currentBottomIndex = currentTopIndex + visibleLogItems.Count - 1;
  210. float firstVisibleLogItemInitialYPos = visibleLogItems[0].Transform.anchoredPosition.y;
  211. for( int i = 0; i < visibleLogItems.Count; i++ )
  212. {
  213. DebugLogItem logItem = visibleLogItems[i];
  214. logItem.Index = currentTopIndex + i;
  215. // If log window is visible, we need to manually refresh the visible items' visual properties. Otherwise, all log items will be refreshed when log window is opened
  216. if( manager.IsLogWindowVisible )
  217. {
  218. RepositionLogItem( logItem );
  219. ColorLogItem( logItem );
  220. // Update collapsed count of the log items in collapsed mode
  221. if( isCollapseOn )
  222. logItem.ShowCount();
  223. }
  224. }
  225. // Shift the ScrollRect
  226. if( !manager.SnapToBottom )
  227. transformComponent.anchoredPosition = new Vector2( 0f, Mathf.Max( 0f, transformComponent.anchoredPosition.y - ( visibleLogItems[0].Transform.anchoredPosition.y - firstVisibleLogItemInitialYPos ) ) );
  228. }
  229. }
  230. private bool ShouldRemoveLogItem( DebugLogItem logItem )
  231. {
  232. if( logItem.Entry.count == 0 )
  233. {
  234. poolLogItemAction( logItem );
  235. return true;
  236. }
  237. return false;
  238. }
  239. private int FindIndexOfLogEntryInReverseDirection( DebugLogEntry logEntry, int startIndex )
  240. {
  241. for( int i = Mathf.Min( startIndex, entriesToShow.Count - 1 ); i >= 0; i-- )
  242. {
  243. if( entriesToShow[i] == logEntry )
  244. return i;
  245. }
  246. return -1;
  247. }
  248. // Log window's width has changed, update the expanded (currently selected) log's height
  249. public void OnViewportWidthChanged()
  250. {
  251. if( indexOfSelectedLogEntry >= entriesToShow.Count )
  252. return;
  253. CalculateSelectedLogEntryHeight();
  254. CalculateContentHeight();
  255. UpdateItemsInTheList( true );
  256. manager.ValidateScrollPosition();
  257. }
  258. // Log window's height has changed, update the list
  259. public void OnViewportHeightChanged()
  260. {
  261. UpdateItemsInTheList( false );
  262. }
  263. private void CalculateContentHeight()
  264. {
  265. float newHeight = Mathf.Max( 1f, entriesToShow.Count * logItemHeight );
  266. if( selectedLogEntry != null )
  267. newHeight += DeltaHeightOfSelectedLogEntry;
  268. transformComponent.sizeDelta = new Vector2( 0f, newHeight );
  269. }
  270. private void CalculateSelectedLogEntryHeight( DebugLogItem referenceItem = null )
  271. {
  272. if( !referenceItem )
  273. {
  274. if( visibleLogItems.Count == 0 )
  275. {
  276. UpdateItemsInTheList( false ); // Try to generate some DebugLogItems, we need one DebugLogItem to calculate the text height
  277. if( visibleLogItems.Count == 0 ) // No DebugLogItems are generated, weird
  278. return;
  279. }
  280. referenceItem = visibleLogItems[0];
  281. }
  282. heightOfSelectedLogEntry = referenceItem.CalculateExpandedHeight( selectedLogEntry, ( timestampsOfEntriesToShow != null ) ? timestampsOfEntriesToShow[indexOfSelectedLogEntry] : (DebugLogEntryTimestamp?) null );
  283. }
  284. // Calculate the indices of log entries to show
  285. // and handle log items accordingly
  286. private void UpdateItemsInTheList( bool updateAllVisibleItemContents )
  287. {
  288. if( entriesToShow.Count > 0 )
  289. {
  290. float contentPosTop = transformComponent.anchoredPosition.y - 1f;
  291. float contentPosBottom = contentPosTop + viewportTransform.rect.height + 2f;
  292. float positionOfSelectedLogEntry = indexOfSelectedLogEntry * logItemHeight;
  293. if( positionOfSelectedLogEntry <= contentPosBottom )
  294. {
  295. if( positionOfSelectedLogEntry <= contentPosTop )
  296. {
  297. contentPosTop = Mathf.Max( contentPosTop - DeltaHeightOfSelectedLogEntry, positionOfSelectedLogEntry - 1f );
  298. contentPosBottom = Mathf.Max( contentPosBottom - DeltaHeightOfSelectedLogEntry, contentPosTop + 2f );
  299. }
  300. else
  301. contentPosBottom = Mathf.Max( contentPosBottom - DeltaHeightOfSelectedLogEntry, positionOfSelectedLogEntry + 1f );
  302. }
  303. int newBottomIndex = Mathf.Min( (int) ( contentPosBottom / logItemHeight ), entriesToShow.Count - 1 );
  304. int newTopIndex = Mathf.Clamp( (int) ( contentPosTop / logItemHeight ), 0, newBottomIndex );
  305. if( currentTopIndex == -1 )
  306. {
  307. // There are no log items visible on screen,
  308. // just create the new log items
  309. updateAllVisibleItemContents = true;
  310. for( int i = 0, count = newBottomIndex - newTopIndex + 1; i < count; i++ )
  311. visibleLogItems.Add( manager.PopLogItem() );
  312. }
  313. else
  314. {
  315. // There are some log items visible on screen
  316. if( newBottomIndex < currentTopIndex || newTopIndex > currentBottomIndex )
  317. {
  318. // If user scrolled a lot such that, none of the log items are now within
  319. // the bounds of the scroll view, pool all the previous log items and create
  320. // new log items for the new list of visible debug entries
  321. updateAllVisibleItemContents = true;
  322. visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );
  323. for( int i = 0, count = newBottomIndex - newTopIndex + 1; i < count; i++ )
  324. visibleLogItems.Add( manager.PopLogItem() );
  325. }
  326. else
  327. {
  328. // User did not scroll a lot such that, there are still some log items within
  329. // the bounds of the scroll view. Don't destroy them but update their content,
  330. // if necessary
  331. if( newTopIndex > currentTopIndex )
  332. visibleLogItems.TrimStart( newTopIndex - currentTopIndex, poolLogItemAction );
  333. if( newBottomIndex < currentBottomIndex )
  334. visibleLogItems.TrimEnd( currentBottomIndex - newBottomIndex, poolLogItemAction );
  335. if( newTopIndex < currentTopIndex )
  336. {
  337. for( int i = 0, count = currentTopIndex - newTopIndex; i < count; i++ )
  338. visibleLogItems.AddFirst( manager.PopLogItem() );
  339. // If it is not necessary to update all the log items,
  340. // then just update the newly created log items. Otherwise,
  341. // wait for the major update
  342. if( !updateAllVisibleItemContents )
  343. UpdateLogItemContentsBetweenIndices( newTopIndex, currentTopIndex - 1, newTopIndex );
  344. }
  345. if( newBottomIndex > currentBottomIndex )
  346. {
  347. for( int i = 0, count = newBottomIndex - currentBottomIndex; i < count; i++ )
  348. visibleLogItems.Add( manager.PopLogItem() );
  349. // If it is not necessary to update all the log items,
  350. // then just update the newly created log items. Otherwise,
  351. // wait for the major update
  352. if( !updateAllVisibleItemContents )
  353. UpdateLogItemContentsBetweenIndices( currentBottomIndex + 1, newBottomIndex, newTopIndex );
  354. }
  355. }
  356. }
  357. currentTopIndex = newTopIndex;
  358. currentBottomIndex = newBottomIndex;
  359. if( updateAllVisibleItemContents )
  360. {
  361. // Update all the log items
  362. UpdateLogItemContentsBetweenIndices( currentTopIndex, currentBottomIndex, newTopIndex );
  363. }
  364. }
  365. else if( currentTopIndex != -1 )
  366. {
  367. // There is nothing to show but some log items are still visible; pool them
  368. visibleLogItems.TrimStart( visibleLogItems.Count, poolLogItemAction );
  369. currentTopIndex = -1;
  370. }
  371. }
  372. private DebugLogItem GetLogItemAtIndex( int index )
  373. {
  374. return visibleLogItems[index - currentTopIndex];
  375. }
  376. private void UpdateLogItemContentsBetweenIndices( int topIndex, int bottomIndex, int logItemOffset )
  377. {
  378. for( int i = topIndex; i <= bottomIndex; i++ )
  379. {
  380. DebugLogItem logItem = visibleLogItems[i - logItemOffset];
  381. logItem.SetContent( entriesToShow[i], ( timestampsOfEntriesToShow != null ) ? timestampsOfEntriesToShow[i] : (DebugLogEntryTimestamp?) null, i, i == indexOfSelectedLogEntry );
  382. RepositionLogItem( logItem );
  383. ColorLogItem( logItem );
  384. if( isCollapseOn )
  385. logItem.ShowCount();
  386. else
  387. logItem.HideCount();
  388. }
  389. }
  390. private void RepositionLogItem( DebugLogItem logItem )
  391. {
  392. int index = logItem.Index;
  393. Vector2 anchoredPosition = new Vector2( 1f, -index * logItemHeight );
  394. if( index > indexOfSelectedLogEntry )
  395. anchoredPosition.y -= DeltaHeightOfSelectedLogEntry;
  396. logItem.Transform.anchoredPosition = anchoredPosition;
  397. }
  398. private void ColorLogItem( DebugLogItem logItem )
  399. {
  400. int index = logItem.Index;
  401. if( index == indexOfSelectedLogEntry )
  402. logItem.Image.color = logItemSelectedColor;
  403. else if( index % 2 == 0 )
  404. logItem.Image.color = logItemNormalColor1;
  405. else
  406. logItem.Image.color = logItemNormalColor2;
  407. }
  408. }
  409. }