//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System.Collections.Generic; namespace BansheeEngine { /** @addtogroup GUI_Engine * @{ */ /// /// GUI element that can efficiently display a list of entries that share the same height. This element is mostly an /// optimization as only visible entries have actual GUI elements, as opposed to just adding GUI elements directly /// in a vertical GUI layout. This allows the list view to have thousands of elements with little performance impact. /// /// Contains shared functionality used by all instances of . /// /// >Type used for storing the data for all list entries. public abstract class GUIListViewBase where TData : GUIListViewData { // TODO - Only fixed size is supported. It should be nice if this object could just be placed in layout like any // other GUI element. Would likely need some kind of a way to get notified when parent layout changes. // (Possibly add a callback to GUIPanel when updateLayout is called?) protected List entries = new List(); protected GUIScrollArea scrollArea; protected GUILabel topPadding; protected GUILabel bottomPadding; protected int width; protected int height; protected int entryHeight; protected float scrollPct; protected bool scrollToLatest; protected int totalHeight; protected internal bool contentsDirty = true; /// /// Total number of entries in the list. /// public int NumEntries { get { return entries.Count; } } /// /// Height of a single entry in the list, in pixels. /// public int EntryHeight { get { return entryHeight; } set { entryHeight = value; } } /// /// Primary GUI scroll area that all entries are contained within. /// internal GUIScrollArea ScrollArea { get { return scrollArea; } } /// /// Creates a new empty list view. /// /// Width of the list view, in pixels. /// Height of the list view, in pixels. /// Height of a single element in the list, in pixels. /// GUI layout into which the list view will be placed into. protected GUIListViewBase(int width, int height, int entryHeight, GUILayout layout) { scrollArea = new GUIScrollArea(ScrollBarType.ShowIfDoesntFit, ScrollBarType.NeverShow, GUIOption.FixedWidth(width), GUIOption.FixedHeight(height)); layout.AddElement(scrollArea); topPadding = new GUILabel(new LocString()); bottomPadding = new GUILabel(new LocString()); scrollArea.Layout.AddElement(topPadding); scrollArea.Layout.AddElement(bottomPadding); this.width = width; this.height = height; this.entryHeight = entryHeight; } /// /// Adds a new entry to the end of the list. /// /// Data of the entry to add. public void AddEntry(TData data) { entries.Add(data); contentsDirty = true; } /// /// Removes an entry from the specified index. If the index is out of range nothing happens. /// /// Sequential index of the element to remove from the list. public void RemoveEntry(int index) { if (index >= 0 && index < entries.Count) { entries.RemoveAt(index); contentsDirty = true; } } /// /// Removes all entries from the list. /// public void Clear() { entries.Clear(); contentsDirty = true; } /// /// Finds an index of the specified entry in the list. /// /// Data of the entry to search for. /// Index of the entry if found, -1 otherwise. public int FindEntry(TData data) { return entries.FindIndex(x => x.Equals(data)); } /// /// 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. /// /// Sequential index at which to insert the entry. /// Data of the entry to insert. public void InsertEntry(int index, TData data) { if (index >= 0 && index <= entries.Count) entries.Insert(index, data); else entries.Add(data); contentsDirty = true; } /// /// Changes the size of the list view. /// /// Width in pixels. /// Height in pixels. public void SetSize(int width, int height) { if (width != this.width || height != this.height) { this.width = width; this.height = height; Rect2I bounds = scrollArea.Bounds; bounds.width = width; bounds.height = height; scrollArea.Bounds = bounds; } } /// /// Updates the visuals of the list view. Should be called once per frame. /// public abstract void Update(); } /// /// GUI element that can efficiently display a list of entries that share the same height. This element is mostly an /// optimization as only visible entries have actual GUI elements, as opposed to just adding GUI elements directly /// in a vertical GUI layout. This allows the list view to have thousands of elements with little performance impact. /// /// Type used for creating and updating the GUI elements of the visible entries. /// Type used for storing the data for all list entries. public class GUIListView : GUIListViewBase where TEntry : GUIListViewEntry, new() where TData : GUIListViewData { private List visibleEntries = new List(); /// /// Creates a new empty list view. /// /// Width of the list view, in pixels. /// Height of the list view, in pixels. /// Height of a single element in the list, in pixels. /// GUI layout into which the list view will be placed into. public GUIListView(int width, int height, int entryHeight, GUILayout layout) :base(width, height, entryHeight, layout) { } /// public override void Update() { int numVisibleEntries = MathEx.CeilToInt(height / (float)entryHeight) + 1; numVisibleEntries = MathEx.Min(numVisibleEntries, entries.Count); while (visibleEntries.Count < numVisibleEntries) { TEntry newEntry = new TEntry(); newEntry.Initialize(this); newEntry.panel.SetHeight(entryHeight); visibleEntries.Add(newEntry); contentsDirty = true; } while (numVisibleEntries < visibleEntries.Count) { int lastIdx = visibleEntries.Count - 1; visibleEntries[lastIdx].Destroy(); visibleEntries.RemoveAt(lastIdx); contentsDirty = true; } if (scrollPct != scrollArea.VerticalScroll) { scrollPct = scrollArea.VerticalScroll; contentsDirty = true; } if (contentsDirty) { int newHeight = entries.Count * entryHeight; if (scrollToLatest) { if (totalHeight > height && scrollPct < 1.0f) scrollToLatest = false; } else { if (totalHeight <= height || scrollPct >= 1.0f) scrollToLatest = true; } totalHeight = newHeight; int maxScrollOffset = MathEx.Max(0, totalHeight - height - 1); float newScrollPct; if (!scrollToLatest) { // Calculate the new scroll pct (which will be active after we change the top/bottom padding element // sizes). If we use the existing scroll pct instead then the elements will lag one frame behind, which // can be very noticeable on quickly updating lists. newScrollPct = (scrollPct*scrollArea.Layout.Bounds.height)/totalHeight; } else newScrollPct = 1.0f; int startPos = MathEx.FloorToInt(newScrollPct * maxScrollOffset); int startIndex = MathEx.FloorToInt(startPos / (float)entryHeight); // Check if we're at the list bottom and the extra element is out of bounds if ((startIndex + visibleEntries.Count) > entries.Count) startIndex--; // Keep the extra element at the top always topPadding.SetHeight(startIndex * entryHeight); for (int i = 0; i < visibleEntries.Count; i++) visibleEntries[i].UpdateContents(startIndex + i, entries[startIndex + i]); int bottomPosition = MathEx.Min(totalHeight, (startIndex + visibleEntries.Count) * entryHeight); bottomPadding.SetHeight(totalHeight - bottomPosition); if (scrollToLatest) { if (newHeight <= height) scrollArea.VerticalScroll = 0.0f; else scrollArea.VerticalScroll = 1.0f; } contentsDirty = false; } } } /// /// Base class that contains data for individual entries used in . /// public class GUIListViewData { } /// /// Base class that displays GUI elements for visible entries used in . /// /// Type of object that contains data used for initializing the GUI elements. public abstract class GUIListViewEntry where TData : GUIListViewData { private GUIListViewBase parent; internal GUIPanel panel; internal GUILayoutY layout; protected GUILayout Layout { get { return layout; } } /// /// Initializes the GUI elements for the entry. /// /// Scroll area into whose layout to insert the GUI elements. internal void Initialize(GUIListViewBase parent) { this.parent = parent; GUIScrollArea scrollArea = parent.ScrollArea; int numElements = scrollArea.Layout.ChildCount; // Last panel is always the padding panel, so keep it there panel = scrollArea.Layout.InsertPanel(numElements - 1); layout = panel.AddLayoutY(); BuildGUI(); } /// /// Destoys the GUI elements for the entry. /// internal void Destroy() { panel.Destroy(); } /// /// Causes all visible entries in the parent list to be updated. /// protected void RefreshEntries() { parent.contentsDirty = true; } /// /// Allows child classes to create GUI elements required by their entry specialization. /// public abstract void BuildGUI(); /// /// Allows child classes to update GUI element(s) with new contents. /// /// Sequential index of the entry in the list. /// Data of the entry to display. public abstract void UpdateContents(int index, TData data); } /** @} */ }