GUIListView.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. using System.Collections.Generic;
  2. namespace BansheeEngine
  3. {
  4. /// <summary>
  5. /// GUI element that can efficiently display a list of entries that share the same height. This element is mostly an
  6. /// optimization as only visible entries have actual GUI elements, as opposed to just adding GUI elements directly
  7. /// in a vertical GUI layout. This allows the list view to have thousands of elements with little performance impact.
  8. /// </summary>
  9. /// <typeparam name="TEntry">Type used for creating and updating the GUI elements of the visible entries.</typeparam>
  10. /// <typeparam name="TData">Type used for storing the data for all list entries.</typeparam>
  11. public class GUIListView<TEntry, TData>
  12. where TEntry : GUIListViewEntry<TData>, new()
  13. where TData : GUIListViewData
  14. {
  15. // TODO - Only fixed size is supported. It should be nice if this object could just be placed in layout like any
  16. // other GUI element. Would likely need some kind of a way to get notified when parent layout changes.
  17. // (Possibly add a callback to GUIPanel when updateLayout is called?)
  18. private List<TEntry> visibleEntries = new List<TEntry>();
  19. private List<TData> entries = new List<TData>();
  20. private GUIScrollArea scrollArea;
  21. private GUILabel topPadding;
  22. private GUILabel bottomPadding;
  23. private int width;
  24. private int height;
  25. private int entryHeight;
  26. private float scrollPct = 0.0f;
  27. private bool scrollToLatest = true;
  28. private bool contentsDirty = true;
  29. /// <summary>
  30. /// Total number of entries in the list.
  31. /// </summary>
  32. public int NumEntries
  33. {
  34. get { return entries.Count; }
  35. }
  36. /// <summary>
  37. /// Height of a single entry in the list, in pixels.
  38. /// </summary>
  39. public int EntryHeight
  40. {
  41. get { return entryHeight; }
  42. set { entryHeight = value; }
  43. }
  44. /// <summary>
  45. /// Creates a new empty list view.
  46. /// </summary>
  47. /// <param name="width">Width of the list view, in pixels.</param>
  48. /// <param name="height">Height of the list view, in pixels.</param>
  49. /// <param name="entryHeight">Height of a single element in the list, in pixels.</param>
  50. /// <param name="layout">GUI layout into which the list view will be placed into.</param>
  51. public GUIListView(int width, int height, int entryHeight, GUILayout layout)
  52. {
  53. scrollArea = new GUIScrollArea(GUIOption.FixedWidth(width), GUIOption.FixedHeight(height));
  54. layout.AddElement(scrollArea);
  55. topPadding = new GUILabel(new LocString());
  56. bottomPadding = new GUILabel(new LocString());
  57. scrollArea.Layout.AddElement(topPadding);
  58. scrollArea.Layout.AddElement(bottomPadding);
  59. this.width = width;
  60. this.height = height;
  61. this.entryHeight = entryHeight;
  62. }
  63. /// <summary>
  64. /// Adds a new entry to the end of the list.
  65. /// </summary>
  66. /// <param name="data">Data of the entry to add.</param>
  67. public void AddEntry(TData data)
  68. {
  69. entries.Add(data);
  70. contentsDirty = true;
  71. }
  72. /// <summary>
  73. /// Removes an entry from the specified index. If the index is out of range nothing happens.
  74. /// </summary>
  75. /// <param name="index">Sequential index of the element to remove from the list.</param>
  76. public void RemoveEntry(int index)
  77. {
  78. if (index >= 0 && index < entries.Count)
  79. {
  80. entries.RemoveAt(index);
  81. contentsDirty = true;
  82. }
  83. }
  84. /// <summary>
  85. /// Removes all entries from the list.
  86. /// </summary>
  87. public void Clear()
  88. {
  89. entries.Clear();
  90. contentsDirty = true;
  91. }
  92. /// <summary>
  93. /// Finds an index of the specified entry in the list.
  94. /// </summary>
  95. /// <param name="data">Data of the entry to search for.</param>
  96. /// <returns>Index of the entry if found, -1 otherwise.</returns>
  97. public int FindEntry(TData data)
  98. {
  99. return entries.FindIndex(x => x.Equals(data));
  100. }
  101. /// <summary>
  102. /// Adds a new entry at the specified index. If the index is out of range the entry is added at the end of the list.
  103. /// </summary>
  104. /// <param name="index">Sequential index at which to insert the entry. </param>
  105. /// <param name="data">Data of the entry to insert.</param>
  106. public void InsertEntry(int index, TData data)
  107. {
  108. if (index >= 0 && index <= entries.Count)
  109. entries.Insert(index, data);
  110. else
  111. entries.Add(data);
  112. contentsDirty = true;
  113. }
  114. /// <summary>
  115. /// Changes the size of the list view.
  116. /// </summary>
  117. /// <param name="width">Width in pixels.</param>
  118. /// <param name="height">Height in pixels.</param>
  119. public void SetSize(int width, int height)
  120. {
  121. if (width != this.width || height != this.height)
  122. {
  123. this.width = width;
  124. this.height = height;
  125. Rect2I bounds = scrollArea.Bounds;
  126. bounds.width = width;
  127. bounds.height = height;
  128. scrollArea.Bounds = bounds;
  129. }
  130. }
  131. /// <summary>
  132. /// Updates the visuals of the list view. Should be called once per frame.
  133. /// </summary>
  134. public void Update()
  135. {
  136. int numVisibleEntries = MathEx.CeilToInt(height / (float)entryHeight) + 1;
  137. numVisibleEntries = MathEx.Min(numVisibleEntries, entries.Count);
  138. while (visibleEntries.Count < numVisibleEntries)
  139. {
  140. TEntry newEntry = new TEntry();
  141. newEntry.Initialize(scrollArea);
  142. newEntry.panel.SetHeight(entryHeight);
  143. visibleEntries.Add(newEntry);
  144. contentsDirty = true;
  145. }
  146. while (numVisibleEntries < visibleEntries.Count)
  147. {
  148. int lastIdx = visibleEntries.Count - 1;
  149. visibleEntries[lastIdx].Destroy();
  150. visibleEntries.RemoveAt(lastIdx);
  151. contentsDirty = true;
  152. }
  153. int totalElementHeight = entries.Count * entryHeight;
  154. if (scrollPct != scrollArea.VerticalScroll)
  155. {
  156. scrollPct = scrollArea.VerticalScroll;
  157. contentsDirty = true;
  158. if (scrollToLatest)
  159. {
  160. if (scrollPct < 1.0f)
  161. scrollToLatest = false;
  162. }
  163. else
  164. {
  165. if (totalElementHeight <= height || scrollPct >= 1.0f)
  166. scrollToLatest = true;
  167. }
  168. }
  169. if (contentsDirty)
  170. {
  171. int maxScrollOffset = MathEx.Max(0, totalElementHeight - height - 1);
  172. int startPos = MathEx.FloorToInt(scrollPct*maxScrollOffset);
  173. int startIndex = MathEx.FloorToInt(startPos/(float)entryHeight);
  174. // Check if we're at the list bottom and the extra element is out of bounds
  175. if ((startIndex + visibleEntries.Count) > entries.Count)
  176. startIndex--; // Keep the extra element at the top always
  177. topPadding.SetHeight(startIndex*entryHeight);
  178. for (int i = 0; i < visibleEntries.Count; i++)
  179. {
  180. visibleEntries[i].UpdateContents(startIndex + i, entries[startIndex + i]);
  181. visibleEntries[i].panel.SetPosition(0, i * entryHeight);
  182. }
  183. int bottomPosition = MathEx.Min(totalElementHeight, (startIndex + visibleEntries.Count)*entryHeight);
  184. bottomPadding.SetHeight(totalElementHeight - bottomPosition);
  185. if (scrollToLatest)
  186. {
  187. if (totalElementHeight <= height)
  188. scrollArea.VerticalScroll = 0.0f;
  189. else
  190. scrollArea.VerticalScroll = 1.0f;
  191. }
  192. contentsDirty = false;
  193. }
  194. }
  195. }
  196. /// <summary>
  197. /// Base class that contains data for individual entries used in <see cref="GUIListView{TEntry,TData}"/>.
  198. /// </summary>
  199. public class GUIListViewData
  200. {
  201. }
  202. /// <summary>
  203. /// Base class that displays GUI elements for visible entries used in <see cref="GUIListView{TEntry,TData}"/>.
  204. /// </summary>
  205. /// <typeparam name="TData">Type of object that contains data used for initializing the GUI elements.</typeparam>
  206. public abstract class GUIListViewEntry<TData>
  207. where TData : GUIListViewData
  208. {
  209. internal GUIPanel panel;
  210. internal GUILayoutY layout;
  211. protected GUILayout Layout { get { return layout; } }
  212. /// <summary>
  213. /// Initializes the GUI elements for the entry.
  214. /// </summary>
  215. /// <param name="parent">Scroll area into whose layout to insert the GUI elements.</param>
  216. internal void Initialize(GUIScrollArea parent)
  217. {
  218. int numElements = parent.Layout.ChildCount;
  219. // Last panel is always the padding panel, so keep it there
  220. panel = parent.Layout.InsertPanel(numElements - 1);
  221. layout = panel.AddLayoutY();
  222. BuildGUI();
  223. }
  224. /// <summary>
  225. /// Destoys the GUI elements for the entry.
  226. /// </summary>
  227. internal void Destroy()
  228. {
  229. panel.Destroy();
  230. }
  231. /// <summary>
  232. /// Allows child classes to create GUI elements required by their entry specialization.
  233. /// </summary>
  234. public abstract void BuildGUI();
  235. /// <summary>
  236. /// Allows child classes to update GUI element(s) with new contents.
  237. /// </summary>
  238. /// <param name="index">Sequential index of the entry in the list.</param>
  239. /// <param name="data">Data of the entry to display.</param>
  240. public abstract void UpdateContents(int index, TData data);
  241. }
  242. }