2
0

GUIListView.cs 13 KB

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