GUIListView.cs 12 KB

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