GUIListView.cs 13 KB

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