diff --git a/NEG/UI/Area/IArea.cs b/NEG/UI/Area/IArea.cs index cc04b03..dd56cbf 100644 --- a/NEG/UI/Area/IArea.cs +++ b/NEG/UI/Area/IArea.cs @@ -8,18 +8,13 @@ namespace NEG.UI.Area public interface IArea : IUiElement { IEnumerable AvailableSlots { get; } - IEnumerable CurrentPopups { get; } - + /// /// Open window /// /// - void OpenWindow(IWindow window); - /// - /// Open popup - /// - /// - void OpenPopup(IPopup popup); + /// + void OpenWindow(IWindow window, object data = null); void CloseAllWindows() { diff --git a/NEG/UI/Area/IArea.cs.meta b/NEG/UI/Area/IArea.cs.meta index 5d555b9..d37d498 100644 --- a/NEG/UI/Area/IArea.cs.meta +++ b/NEG/UI/Area/IArea.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: ffa2155176774ab691d499979707a1bb +guid: f7cf5ef3a347e1c4b98411f4d564b988 timeCreated: 1670690282 \ No newline at end of file diff --git a/NEG/UI/Area/MonoArea.cs b/NEG/UI/Area/MonoArea.cs deleted file mode 100644 index 9f13ef3..0000000 --- a/NEG/UI/Area/MonoArea.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using NEG.UI.Popup; -using NEG.UI.Window; -using NEG.UI.WindowSlot; - -namespace NEG.UI.Area -{ - public class MonoArea : MonoBehaviour, IArea - { - public IEnumerable AvailableSlots => windowSlots; - public IWindowSlot DefaultWindowSlot => windowSlots[0]; - public IEnumerable CurrentWindows { get; } - public IEnumerable CurrentPopups => currentPopups; - - [SerializeField] private List windowSlots; - - [SerializeField] private Queue currentPopups = new(); - - public void SetEnabled(bool setEnabled) => gameObject.SetActive(setEnabled); - - public void OpenWindow(IWindow window) => DefaultWindowSlot.AttachWindow(window); - - public void OpenPopup(IPopup popup) - { - currentPopups.Enqueue(popup); - popup.OnPopupClosed += OnPopupClosed; - popup.Show(); - UpdatePopupStates(); - } - - private void UpdatePopupStates() - { - if(currentPopups.Count == 0) - return; - - while (currentPopups.TryPeek(out var popup)) - { - if(popup.IsValid) - popup.SetEnabled(true); - - currentPopups.Dequeue(); - } - } - - private void OnPopupClosed(IPopup popup) - { - if (!currentPopups.Contains(popup)) - return; - - if(currentPopups.Peek() != popup) - return; - - currentPopups.Dequeue(); - UpdatePopupStates(); - } - } -} \ No newline at end of file diff --git a/NEG/UI/NEG.UI.asmdef b/NEG/UI/NEG.UI.asmdef index df5cf87..db95a9f 100644 --- a/NEG/UI/NEG.UI.asmdef +++ b/NEG/UI/NEG.UI.asmdef @@ -1,5 +1,5 @@ { - "name": "NegUi", + "name": "NEG.UI", "rootNamespace": "", "references": [], "includePlatforms": [], diff --git a/NEG/UI/Popup/DefaultPopupData.cs b/NEG/UI/Popup/DefaultPopupData.cs new file mode 100644 index 0000000..bcbbefb --- /dev/null +++ b/NEG/UI/Popup/DefaultPopupData.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace NEG.UI.Popup +{ + public class DefaultPopupData : PopupData + { + private readonly IDefaultPopup defaultPopup; + + private readonly string title; + private readonly string content; + private readonly List<(string, Action)> options; + + public DefaultPopupData(IDefaultPopup popup, string title, string content, List<(string, Action)> options) : base(popup) + { + defaultPopup = popup; + this.title = title; + this.content = content; + this.options = options; + } + + public override void Show() + { + defaultPopup.SetContent(title, content, options); + base.Show(); + } + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/DefaultPopupData.cs.meta b/NEG/UI/Popup/DefaultPopupData.cs.meta new file mode 100644 index 0000000..e86aa41 --- /dev/null +++ b/NEG/UI/Popup/DefaultPopupData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 372c6df9ec044cb3adc86c7776b2ef61 +timeCreated: 1672432934 \ No newline at end of file diff --git a/NEG/UI/Popup/IDefaultPopup.cs b/NEG/UI/Popup/IDefaultPopup.cs new file mode 100644 index 0000000..89ac826 --- /dev/null +++ b/NEG/UI/Popup/IDefaultPopup.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace NEG.UI.Popup +{ + public interface IDefaultPopup : IPopup + { + public void SetContent(string title, string content, List<(string, Action)> options); + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/IDefaultPopup.cs.meta b/NEG/UI/Popup/IDefaultPopup.cs.meta new file mode 100644 index 0000000..e510e78 --- /dev/null +++ b/NEG/UI/Popup/IDefaultPopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a6d0871ada44d1d95bea6c8e8701769 +timeCreated: 1672153906 \ No newline at end of file diff --git a/NEG/UI/Popup/IPopup.cs b/NEG/UI/Popup/IPopup.cs index 25b2025..246272a 100644 --- a/NEG/UI/Popup/IPopup.cs +++ b/NEG/UI/Popup/IPopup.cs @@ -5,41 +5,18 @@ using System; namespace NEG.UI.Popup { [PublicAPI] - public interface IPopup : IUiElement + public interface IPopup { /// - /// Call when popup is not forcly closed + /// Show popup /// - event Action OnPopupClosed; - - /// - /// Is popup still valid to show - /// - bool IsValid { get; } - - /// - /// Mark popup as ready to show. - /// - internal void Show(); - + /// data assigned to popup, used to give info that popup is closed + void Show(PopupData data); + /// /// Close popup or mark as closed if not visible /// - /// Is closing by forced by system, ex. close area - void Close(bool isForced = false); - } - - public static class PopupExtensions - { - public static void Show(this IPopup popup, IArea area = null) - { - if (area != null) - { - area.OpenPopup(popup); - return; - } - - UiManager.Instance.CurrentArea.OpenPopup(popup); - } + /// if true hide visually, without firing callbacks + void Close(bool silence = false); } } \ No newline at end of file diff --git a/NEG/UI/Popup/MonoPopup.cs b/NEG/UI/Popup/MonoPopup.cs deleted file mode 100644 index 8277bf3..0000000 --- a/NEG/UI/Popup/MonoPopup.cs +++ /dev/null @@ -1,24 +0,0 @@ -using NEG.UI.Area; -using System; -using UnityEngine; - -namespace NEG.UI.Popup -{ - public class MonoPopup : MonoBehaviour, IPopup - { - public event Action OnPopupClosed; - public bool IsValid { get; private set; } - - public void Close(bool isForced = false) - { - IsValid = false; - gameObject.SetActive(false); - if(!isForced) - OnPopupClosed?.Invoke(this); - } - - public void SetEnabled(bool setEnabled) => gameObject.SetActive(setEnabled); - - void IPopup.Show() => IsValid = true; - } -} \ No newline at end of file diff --git a/NEG/UI/Popup/PopupData.cs b/NEG/UI/Popup/PopupData.cs new file mode 100644 index 0000000..cd8bd24 --- /dev/null +++ b/NEG/UI/Popup/PopupData.cs @@ -0,0 +1,20 @@ +namespace NEG.UI.Popup +{ + public class PopupData + { + public bool IsValid { get; protected set; } + + private IPopup popup; + + public PopupData(IPopup popup) + { + this.popup = popup; + IsValid = true; + } + + public virtual void Show() => popup.Show(this); + public virtual void Hide() => popup.Close(true); + + public virtual void Invalidate() => IsValid = false; + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/PopupData.cs.meta b/NEG/UI/Popup/PopupData.cs.meta new file mode 100644 index 0000000..2148759 --- /dev/null +++ b/NEG/UI/Popup/PopupData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6e5ebb367aa4dfba5e5f853c9b31a3d +timeCreated: 1672430446 \ No newline at end of file diff --git a/NEG/UI/PriorityQueue.cs b/NEG/UI/PriorityQueue.cs new file mode 100644 index 0000000..38ca145 --- /dev/null +++ b/NEG/UI/PriorityQueue.cs @@ -0,0 +1,999 @@ +#nullable enable +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ported from: +// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs + internal sealed class PriorityQueueDebugView + { + private readonly PriorityQueue _queue; + private readonly bool _sort; + + public PriorityQueueDebugView(PriorityQueue queue) + { + ArgumentNullException.ThrowIfNull(queue); + + _queue = queue; + _sort = true; + } + + public PriorityQueueDebugView(PriorityQueue.UnorderedItemsCollection collection) + { + _queue = collection?._queue ?? throw new System.ArgumentNullException(nameof(collection)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public (TElement Element, TPriority Priority)[] Items + { + get + { + List<(TElement Element, TPriority Priority)> list = new(_queue.UnorderedItems); + if (_sort) list.Sort((i1, i2) => _queue.Comparer.Compare(i1.Priority, i2.Priority)); + + return list.ToArray(); + } + } + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class SR + { + internal const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; + internal const string ArgumentOutOfRange_IndexMustBeLessOrEqual = "Index must be less or equal"; + internal const string InvalidOperation_EmptyQueue = "The queue is empty."; + internal const string InvalidOperation_EnumFailedVersion = "Collection modified while iterating over it."; + internal const string Arg_NonZeroLowerBound = "Non-zero lower bound required."; + internal const string Arg_RankMultiDimNotSupported = "Multi-dimensional arrays not supported."; + internal const string Argument_InvalidArrayType = "Invalid array type."; + internal const string Argument_InvalidOffLen = "Invalid offset or length."; + } + + internal static class ArgumentNullException + { + public static void ThrowIfNull(object o) + { + if (o == null) + throw new System.ArgumentNullException(); // hard to do it differently without C# 10's features + } + } + + internal static class ArrayEx + { + internal const int MaxLength = int.MaxValue; + } + + /// + /// Internal helper functions for working with enumerables. + /// + internal static class EnumerableHelpers + { + /// Converts an enumerable to an array using the same logic as List{T}. + /// The enumerable to convert. + /// The number of items stored in the resulting array, 0-indexed. + /// + /// The resulting array. The length of the array may be greater than , + /// which is the actual number of elements in the array. + /// + internal static T[] ToArray(IEnumerable source, out int length) + { + if (source is ICollection ic) + { + int count = ic.Count; + if (count != 0) + { + // Allocate an array of the desired size, then copy the elements into it. Note that this has the same + // issue regarding concurrency as other existing collections like List. If the collection size + // concurrently changes between the array allocation and the CopyTo, we could end up either getting an + // exception from overrunning the array (if the size went up) or we could end up not filling as many + // items as 'count' suggests (if the size went down). This is only an issue for concurrent collections + // that implement ICollection, which as of .NET 4.6 is just ConcurrentDictionary. + var arr = new T[count]; + ic.CopyTo(arr, 0); + length = count; + return arr; + } + } + else + { + using (var en = source.GetEnumerator()) + { + if (en.MoveNext()) + { + const int DefaultCapacity = 4; + var arr = new T[DefaultCapacity]; + arr[0] = en.Current; + int count = 1; + + while (en.MoveNext()) + { + if (count == arr.Length) + { + // This is the same growth logic as in List: + // If the array is currently empty, we make it a default size. Otherwise, we attempt to + // double the size of the array. Doubling will overflow once the size of the array reaches + // 2^30, since doubling to 2^31 is 1 larger than Int32.MaxValue. In that case, we instead + // constrain the length to be Array.MaxLength (this overflow check works because of the + // cast to uint). + int newLength = count << 1; + if ((uint)newLength > ArrayEx.MaxLength) + newLength = ArrayEx.MaxLength <= count ? count + 1 : ArrayEx.MaxLength; + + Array.Resize(ref arr, newLength); + } + + arr[count++] = en.Current; + } + + length = count; + return arr; + } + } + } + + length = 0; + return Array.Empty(); + } + } + + /// + /// Represents a min priority queue. + /// + /// Specifies the type of elements in the queue. + /// Specifies the type of priority associated with enqueued elements. + /// + /// Implements an array-backed quaternary min-heap. Each element is enqueued with an associated priority + /// that determines the dequeue order: elements with the lowest priority get dequeued first. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] + public class PriorityQueue + { + /// + /// Specifies the arity of the d-ary heap, which here is quaternary. + /// It is assumed that this value is a power of 2. + /// + private const int Arity = 4; + + /// + /// The binary logarithm of . + /// + private const int Log2Arity = 2; + + /// + /// Custom comparer used to order the heap. + /// + private readonly IComparer? _comparer; + + /// + /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. + /// + private (TElement Element, TPriority Priority)[] _nodes; + + /// + /// The number of nodes in the heap. + /// + private int _size; + + /// + /// Lazily-initialized collection used to expose the contents of the queue. + /// + private UnorderedItemsCollection? _unorderedItems; + + /// + /// Version updated on mutation to help validate enumerators operate on a consistent state. + /// + private int _version; + +#if DEBUG + static PriorityQueue() + { + Debug.Assert(Log2Arity > 0 && Math.Pow(2, Log2Arity) == Arity); + } +#endif + + /// + /// Initializes a new instance of the class. + /// + public PriorityQueue() + { + _nodes = Array.Empty<(TElement, TPriority)>(); + _comparer = InitializeComparer(null); + } + + /// + /// Initializes a new instance of the class + /// with the specified initial capacity. + /// + /// Initial capacity to allocate in the underlying heap array. + /// + /// The specified was negative. + /// + public PriorityQueue(int initialCapacity) + : this(initialCapacity, null) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified custom priority comparer. + /// + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + public PriorityQueue(IComparer? comparer) + { + _nodes = Array.Empty<(TElement, TPriority)>(); + _comparer = InitializeComparer(comparer); + } + + /// + /// Initializes a new instance of the class + /// with the specified initial capacity and custom priority comparer. + /// + /// Initial capacity to allocate in the underlying heap array. + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + /// + /// The specified was negative. + /// + public PriorityQueue(int initialCapacity, IComparer? comparer) + { + if (initialCapacity < 0) + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum); + + _nodes = new (TElement, TPriority)[initialCapacity]; + _comparer = InitializeComparer(comparer); + } + + /// + /// Initializes a new instance of the class + /// that is populated with the specified elements and priorities. + /// + /// The pairs of elements and priorities with which to populate the queue. + /// + /// The specified argument was . + /// + /// + /// Constructs the heap using a heapify operation, + /// which is generally faster than enqueuing individual elements sequentially. + /// + public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items) + : this(items, null) + { + } + + /// + /// Initializes a new instance of the class + /// that is populated with the specified elements and priorities, + /// and with the specified custom priority comparer. + /// + /// The pairs of elements and priorities with which to populate the queue. + /// + /// Custom comparer dictating the ordering of elements. + /// Uses if the argument is . + /// + /// + /// The specified argument was . + /// + /// + /// Constructs the heap using a heapify operation, + /// which is generally faster than enqueuing individual elements sequentially. + /// + public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items, IComparer? comparer) + { + ArgumentNullException.ThrowIfNull(items); + + _nodes = EnumerableHelpers.ToArray(items, out _size); + _comparer = InitializeComparer(comparer); + + if (_size > 1) Heapify(); + } + + /// + /// Gets the number of elements contained in the . + /// + public int Count => _size; + + /// + /// Gets the priority comparer used by the . + /// + public IComparer Comparer => _comparer ?? Comparer.Default; + + /// + /// Gets a collection that enumerates the elements of the queue in an unordered manner. + /// + /// + /// The enumeration does not order items by priority, since that would require N * log(N) time and N space. + /// Items are instead enumerated following the internal array heap layout. + /// + public UnorderedItemsCollection UnorderedItems => _unorderedItems ??= new UnorderedItemsCollection(this); + + /// + /// Adds the specified element with associated priority to the . + /// + /// The element to add to the . + /// The priority with which to associate the new element. + public void Enqueue(TElement element, TPriority priority) + { + // Virtually add the node at the end of the underlying array. + // Note that the node being enqueued does not need to be physically placed + // there at this point, as such an assignment would be redundant. + + int currentSize = _size++; + _version++; + + if (_nodes.Length == currentSize) Grow(currentSize + 1); + + if (_comparer == null) + MoveUpDefaultComparer((element, priority), currentSize); + else + MoveUpCustomComparer((element, priority), currentSize); + } + + /// + /// Returns the minimal element from the without removing it. + /// + /// The is empty. + /// The minimal element of the . + public TElement Peek() + { + if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + + return _nodes[0].Element; + } + + /// + /// Removes and returns the minimal element from the . + /// + /// The queue is empty. + /// The minimal element of the . + public TElement Dequeue() + { + if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + + var element = _nodes[0].Element; + RemoveRootNode(); + return element; + } + + /// + /// Removes the minimal element from the , + /// and copies it to the parameter, + /// and its associated priority to the parameter. + /// + /// The removed element. + /// The priority associated with the removed element. + /// + /// if the element is successfully removed; + /// if the is empty. + /// + public bool TryDequeue([MaybeNullWhen(false)] out TElement element, + [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[0]; + RemoveRootNode(); + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Returns a value that indicates whether there is a minimal element in the + /// , + /// and if one is present, copies it to the parameter, + /// and its associated priority to the parameter. + /// The element is not removed from the . + /// + /// The minimal element in the queue. + /// The priority associated with the minimal element. + /// + /// if there is a minimal element; + /// if the is empty. + /// + public bool TryPeek([MaybeNullWhen(false)] out TElement element, + [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[0]; + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Adds the specified element with associated priority to the , + /// and immediately removes the minimal element, returning the result. + /// + /// The element to add to the . + /// The priority with which to associate the new element. + /// The minimal element removed after the enqueue operation. + /// + /// Implements an insert-then-extract heap operation that is generally more efficient + /// than sequencing Enqueue and Dequeue operations: in the worst case scenario only one + /// shift-down operation is required. + /// + public TElement EnqueueDequeue(TElement element, TPriority priority) + { + if (_size != 0) + { + var root = _nodes[0]; + + if (_comparer == null) + { + if (Comparer.Default.Compare(priority, root.Priority) > 0) + { + MoveDownDefaultComparer((element, priority), 0); + _version++; + return root.Element; + } + } + else + { + if (_comparer.Compare(priority, root.Priority) > 0) + { + MoveDownCustomComparer((element, priority), 0); + _version++; + return root.Element; + } + } + } + + return element; + } + + /// + /// Enqueues a sequence of element/priority pairs to the . + /// + /// The pairs of elements and priorities to add to the queue. + /// + /// The specified argument was . + /// + public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> items) + { + ArgumentNullException.ThrowIfNull(items); + + int count = 0; + var collection = + items as ICollection<(TElement Element, TPriority Priority)>; + if (collection is not null && (count = collection.Count) > _nodes.Length - _size) Grow(_size + count); + + if (_size == 0) + { + // build using Heapify() if the queue is empty. + + if (collection is not null) + { + collection.CopyTo(_nodes, 0); + _size = count; + } + else + { + int i = 0; + (TElement, TPriority)[] nodes = _nodes; + foreach ((var element, var priority) in items) + { + if (nodes.Length == i) + { + Grow(i + 1); + nodes = _nodes; + } + + nodes[i++] = (element, priority); + } + + _size = i; + } + + _version++; + + if (_size > 1) Heapify(); + } + else + { + foreach ((var element, var priority) in items) Enqueue(element, priority); + } + } + + /// + /// Enqueues a sequence of elements pairs to the , + /// all associated with the specified priority. + /// + /// The elements to add to the queue. + /// The priority to associate with the new elements. + /// + /// The specified argument was . + /// + public void EnqueueRange(IEnumerable elements, TPriority priority) + { + ArgumentNullException.ThrowIfNull(elements); + + int count; + if (elements is ICollection<(TElement Element, TPriority Priority)> collection && + (count = collection.Count) > _nodes.Length - _size) + Grow(_size + count); + + if (_size == 0) + { + // build using Heapify() if the queue is empty. + + int i = 0; + (TElement, TPriority)[] nodes = _nodes; + foreach (var element in elements) + { + if (nodes.Length == i) + { + Grow(i + 1); + nodes = _nodes; + } + + nodes[i++] = (element, priority); + } + + _size = i; + _version++; + + if (i > 1) Heapify(); + } + else + { + foreach (var element in elements) Enqueue(element, priority); + } + } + + /// + /// Removes all items from the . + /// + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) + // Clear the elements so that the gc can reclaim the references + Array.Clear(_nodes, 0, _size); + + _size = 0; + _version++; + } + + /// + /// Ensures that the can hold up to + /// items without further expansion of its backing storage. + /// + /// The minimum capacity to be used. + /// + /// The specified is negative. + /// + /// The current capacity of the . + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); + + if (_nodes.Length < capacity) + { + Grow(capacity); + _version++; + } + + return _nodes.Length; + } + + /// + /// Sets the capacity to the actual number of items in the , + /// if that is less than 90 percent of current capacity. + /// + /// + /// This method can be used to minimize a collection's memory overhead + /// if no new elements will be added to the collection. + /// + public void TrimExcess() + { + int threshold = (int)(_nodes.Length * 0.9); + if (_size < threshold) + { + Array.Resize(ref _nodes, _size); + _version++; + } + } + + /// + /// Grows the priority queue to match the specified min capacity. + /// + private void Grow(int minCapacity) + { + Debug.Assert(_nodes.Length < minCapacity); + + const int GrowFactor = 2; + const int MinimumGrow = 4; + + int newcapacity = GrowFactor * _nodes.Length; + + // Allow the queue to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _nodes.Length overflowed thanks to the (uint) cast + if ((uint)newcapacity > ArrayEx.MaxLength) newcapacity = ArrayEx.MaxLength; + + // Ensure minimum growth is respected. + newcapacity = Math.Max(newcapacity, _nodes.Length + MinimumGrow); + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newcapacity < minCapacity) newcapacity = minCapacity; + + Array.Resize(ref _nodes, newcapacity); + } + + /// + /// Removes the node from the root of the heap + /// + private void RemoveRootNode() + { + int lastNodeIndex = --_size; + _version++; + + if (lastNodeIndex > 0) + { + var lastNode = _nodes[lastNodeIndex]; + if (_comparer == null) + MoveDownDefaultComparer(lastNode, 0); + else + MoveDownCustomComparer(lastNode, 0); + } + + if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) + _nodes[lastNodeIndex] = default; + } + + /// + /// Gets the index of an element's parent. + /// + private static int GetParentIndex(int index) => (index - 1) >> Log2Arity; + + /// + /// Gets the index of the first child of an element. + /// + private static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; + + /// + /// Converts an unordered list into a heap. + /// + private void Heapify() + { + // Leaves of the tree are in fact 1-element heaps, for which there + // is no need to correct them. The heap property needs to be restored + // only for higher nodes, starting from the first node that has children. + // It is the parent of the very last element in the array. + + var nodes = _nodes; + int lastParentWithChildren = GetParentIndex(_size - 1); + + if (_comparer == null) + for (int index = lastParentWithChildren; index >= 0; --index) + MoveDownDefaultComparer(nodes[index], index); + else + for (int index = lastParentWithChildren; index >= 0; --index) + MoveDownCustomComparer(nodes[index], index); + } + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUpDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + Debug.Assert(_comparer is null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + + while (nodeIndex > 0) + { + int parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (Comparer.Default.Compare(node.Priority, parent.Priority) < 0) + { + nodes[nodeIndex] = parent; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUpCustomComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + Debug.Assert(_comparer is not null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var comparer = _comparer; + var nodes = _nodes; + + while (nodeIndex > 0) + { + int parentIndex = GetParentIndex(nodeIndex); + var parent = nodes[parentIndex]; + + if (comparer.Compare(node.Priority, parent.Priority) < 0) + { + nodes[nodeIndex] = parent; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDownDefaultComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(_comparer is null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var nodes = _nodes; + int size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the minimal priority + var minChild = nodes[i]; + int minChildIndex = i; + + int childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (Comparer.Default.Compare(nextChild.Priority, minChild.Priority) < 0) + { + minChild = nextChild; + minChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (Comparer.Default.Compare(node.Priority, minChild.Priority) <= 0) break; + + // Move the minimal child up by one node and + // continue recursively from its location. + nodes[nodeIndex] = minChild; + nodeIndex = minChildIndex; + } + + nodes[nodeIndex] = node; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDownCustomComparer((TElement Element, TPriority Priority) node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + Debug.Assert(_comparer is not null); + Debug.Assert(0 <= nodeIndex && nodeIndex < _size); + + var comparer = _comparer; + var nodes = _nodes; + int size = _size; + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < size) + { + // Find the child node with the minimal priority + var minChild = nodes[i]; + int minChildIndex = i; + + int childIndexUpperBound = Math.Min(i + Arity, size); + while (++i < childIndexUpperBound) + { + var nextChild = nodes[i]; + if (comparer.Compare(nextChild.Priority, minChild.Priority) < 0) + { + minChild = nextChild; + minChildIndex = i; + } + } + + // Heap property is satisfied; insert node in this location. + if (comparer.Compare(node.Priority, minChild.Priority) <= 0) break; + + // Move the minimal child up by one node and continue recursively from its location. + nodes[nodeIndex] = minChild; + nodeIndex = minChildIndex; + } + + nodes[nodeIndex] = node; + } + + /// + /// Initializes the custom comparer to be used internally by the heap. + /// + private static IComparer? InitializeComparer(IComparer? comparer) + { + if (typeof(TPriority).IsValueType) + { + if (comparer == Comparer.Default) + // if the user manually specifies the default comparer, + // revert to using the optimized path. + return null; + + return comparer; + } + + // Currently the JIT doesn't optimize direct Comparer.Default.Compare + // calls for reference types, so we want to cache the comparer instance instead. + // TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future. + return comparer ?? Comparer.Default; + } + + /// + /// Enumerates the contents of a , without any ordering guarantees. + /// + [DebuggerDisplay("Count = {Count}")] + [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] + public sealed class UnorderedItemsCollection : IReadOnlyCollection<(TElement Element, TPriority Priority)>, + ICollection + { + internal readonly PriorityQueue _queue; + + internal UnorderedItemsCollection(PriorityQueue queue) + { + _queue = queue; + } + + object ICollection.SyncRoot => this; + bool ICollection.IsSynchronized => false; + + void ICollection.CopyTo(Array array, int index) + { + ArgumentNullException.ThrowIfNull(array); + + if (array.Rank != 1) throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + + if (array.GetLowerBound(0) != 0) throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); + + if (index < 0 || index > array.Length) + throw new ArgumentOutOfRangeException(nameof(index), index, + SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); + + if (array.Length - index < _queue._size) throw new ArgumentException(SR.Argument_InvalidOffLen); + + try + { + Array.Copy(_queue._nodes, 0, array, index, _queue._size); + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + } + + public int Count => _queue._size; + + IEnumerator<(TElement Element, TPriority Priority)> IEnumerable<(TElement Element, TPriority Priority)>. + GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the . + /// + /// An for the . + public Enumerator GetEnumerator() => new(_queue); + + /// + /// Enumerates the element and priority pairs of a , + /// without any ordering guarantees. + /// + public struct Enumerator : IEnumerator<(TElement Element, TPriority Priority)> + { + private readonly PriorityQueue _queue; + private readonly int _version; + private int _index; + + internal Enumerator(PriorityQueue queue) + { + _queue = queue; + _index = 0; + _version = queue._version; + Current = default; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + var localQueue = _queue; + + if (_version == localQueue._version && (uint)_index < (uint)localQueue._size) + { + Current = localQueue._nodes[_index]; + _index++; + return true; + } + + return MoveNextRare(); + } + + private bool MoveNextRare() + { + if (_version != _queue._version) + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + + _index = _queue._size + 1; + Current = default; + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public (TElement Element, TPriority Priority) Current { get; private set; } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + if (_version != _queue._version) + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + + _index = 0; + Current = default; + } + } + } + } +} \ No newline at end of file diff --git a/NEG/UI/PriorityQueue.cs.meta b/NEG/UI/PriorityQueue.cs.meta new file mode 100644 index 0000000..28b7de0 --- /dev/null +++ b/NEG/UI/PriorityQueue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1b6a9a70d997fd468f30caa1e760078 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/UiManager.cs b/NEG/UI/UiManager.cs index 435fd28..76a4921 100644 --- a/NEG/UI/UiManager.cs +++ b/NEG/UI/UiManager.cs @@ -1,13 +1,16 @@ using NEG.UI.Area; +using NEG.UI.Popup; +using System; using System.Collections.Generic; using UnityEngine; namespace NEG.UI { - public class UiManager + public abstract class UiManager { //TODO: use default unity selection //TODO: window snaping to slots + //TODO: Default prefabs? public static UiManager Instance { get; private set; } @@ -31,8 +34,15 @@ namespace NEG.UI } private IArea currentArea; + protected IDefaultPopup currentDefaultPopup; + private (PopupData data, int priority) currentShownPopup; - public UiManager(IArea startArea) + private PriorityQueue popupsToShow = new(); + + //TODO: localize + private string localizedYes = "Yes", localizedNo = "No", localizedOk = "Ok"; + + protected UiManager(IArea startArea) { if (Instance != null) { @@ -44,6 +54,107 @@ namespace NEG.UI CurrentArea = startArea; } + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing ok button. + /// + /// popup title + /// popup content + /// text to show on ok button, empty for localized "Ok" + /// additional action on ok pressed + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowOkPopup(string title, string content, string okText = null, Action okPressed = null, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, + new List<(string, Action)>() { (okText ?? localizedOk, okPressed) }); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing yes or no button. + /// + /// popup title + /// popup content + /// text to show on yes button, empty for localized "Yes" + /// text to show on no button, empty for localized "No" + /// additional action on yes pressed + /// additional action on no pressed + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowYesNoPopup(string title, string content, string yesText = null, string noText = null, Action yesPressed = null, Action noPressed = null, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, + new List<(string, Action)>() { (yesText ?? localizedYes, yesPressed), (noText ?? localizedNo, noPressed) }); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. It will be closed after pressing any button. + /// + /// popup title + /// popup content + /// list of actions + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + /// data for created popup, can be used to invalidate popup (will not show) + public PopupData ShowPopup(string title, string content, List<(string, Action)> actions, int priority = 0, bool forceShow = false) + { + var data = new DefaultPopupData(currentDefaultPopup, title, content, actions); + ShowPopup(data, priority, forceShow); + return data; + } + + /// + /// Show popup if there is non other currently shown. Otherwise add current popup to ordered queue and show it later. + /// + /// popup data object + /// priority of popup (lower number -> show first) + /// force show current popup only if currently shown has lower priority + public void ShowPopup(PopupData data, int priority = 0, bool forceShow = false) + { + popupsToShow.Enqueue(data, priority); + UpdatePopupsState(forceShow, priority, data); + } + + public void PopupClosed(PopupData data) + { + if (currentShownPopup.data != data) + { + Debug.LogError("Popup was not shown"); + return; + } + UpdatePopupsState(false); + } + + protected void SetDefaultPopup(IDefaultPopup popup) => currentDefaultPopup = popup; + + private void UpdatePopupsState(bool forceShow, int priority = 0, PopupData data = null) + { + if (forceShow) + { + if(currentShownPopup.priority <= priority) + return; + + popupsToShow.Enqueue(currentShownPopup.data, currentShownPopup.priority); + ShowPopup(data, priority); + return; + } + + if(!popupsToShow.TryDequeue(out var d, out int p)) + return; + + ShowPopup(d, p); + } + + private void ShowPopup(PopupData data, int priority) + { + currentShownPopup = (data, priority); + data.Show(); + } } } diff --git a/NEG/UI/UnityUi/Area.meta b/NEG/UI/UnityUi/Area.meta new file mode 100644 index 0000000..abc61b9 --- /dev/null +++ b/NEG/UI/UnityUi/Area.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a99aed857b7b9e7459811b14fefdb04f +timeCreated: 1670706939 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Area/MonoArea.cs b/NEG/UI/UnityUi/Area/MonoArea.cs new file mode 100644 index 0000000..7691c08 --- /dev/null +++ b/NEG/UI/UnityUi/Area/MonoArea.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using UnityEngine; +using NEG.UI.Popup; +using NEG.UI.UnityUi.WindowSlot; +using NEG.UI.Window; +using NEG.UI.WindowSlot; + +namespace NEG.UI.Area +{ + public class MonoArea : MonoBehaviour, IArea + { + public IEnumerable AvailableSlots => windowSlots; + public IWindowSlot DefaultWindowSlot => windowSlots[0]; + public IEnumerable CurrentWindows { get; } + + [SerializeField] private List windowSlots; + + [SerializeField] private Queue currentPopups = new(); + + public void SetEnabled(bool setEnabled) => gameObject.SetActive(setEnabled); + + public void OpenWindow(IWindow window, object data = null) + { + DefaultWindowSlot.AttachWindow(window); + window.SetData(data); + } + + + } +} \ No newline at end of file diff --git a/NEG/UI/Area/MonoArea.cs.meta b/NEG/UI/UnityUi/Area/MonoArea.cs.meta similarity index 54% rename from NEG/UI/Area/MonoArea.cs.meta rename to NEG/UI/UnityUi/Area/MonoArea.cs.meta index 805b8e3..958b733 100644 --- a/NEG/UI/Area/MonoArea.cs.meta +++ b/NEG/UI/UnityUi/Area/MonoArea.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: bbfe2293348b4c9096a3763479ec00c7 +guid: 39eb59ca1ef60934abb3f0c64169be65 timeCreated: 1670707479 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/BaseButton.cs b/NEG/UI/UnityUi/Buttons/BaseButton.cs index 13c2361..35dfbe9 100644 --- a/NEG/UI/UnityUi/Buttons/BaseButton.cs +++ b/NEG/UI/UnityUi/Buttons/BaseButton.cs @@ -12,6 +12,9 @@ namespace NEG.UI.UnityUi.Buttons { public event Action OnButtonPressed; + public bool Interactable { get => serializeFields.Button.interactable; set => serializeFields.Button.interactable = value; } + + [SerializeField] protected ButtonSerializeFields serializeFields; private bool isHovered; @@ -42,13 +45,29 @@ namespace NEG.UI.UnityUi.Buttons serializeFields.Text.color = serializeFields.DeselectedTextColor; } + public void SetText(string text) + { + if(serializeFields == null) + return; + if(serializeFields.Text == null) + return; + serializeFields.Text.text = text; + } + protected virtual void Awake() { - serializeFields = GetComponent(); + if(serializeFields == null) + serializeFields = GetComponent(); serializeFields.Button.onClick.AddListener(OnClicked); OnDeselect(null); } + private void OnValidate() + { + if(serializeFields == null) + serializeFields = GetComponent(); + } + protected virtual void OnClicked() { OnDeselect(null); diff --git a/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs b/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs new file mode 100644 index 0000000..fb8954a --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace NEG.UI.UnityUi.Buttons +{ + [RequireComponent(typeof(BaseButton))] + public class CloseAllWindows : MonoBehaviour + { + private void Awake() => GetComponent().OnButtonPressed += OnClicked; + private void OnClicked() => UiManager.Instance.CurrentArea.CloseAllWindows(); + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs.meta b/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs.meta new file mode 100644 index 0000000..4f29dac --- /dev/null +++ b/NEG/UI/UnityUi/Buttons/CloseAllWindows.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2fa43803dc014b7694d265d48c1fc680 +timeCreated: 1672767354 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Buttons/CloseWindow.cs b/NEG/UI/UnityUi/Buttons/CloseWindow.cs index 90c7ebb..2fdc9ae 100644 --- a/NEG/UI/UnityUi/Buttons/CloseWindow.cs +++ b/NEG/UI/UnityUi/Buttons/CloseWindow.cs @@ -1,4 +1,5 @@ -using NEG.UI.Window; +using NEG.UI.UnityUi.Window; +using NEG.UI.Window; using System; using UnityEngine; diff --git a/NEG/UI/UnityUi/Buttons/OpenWindow.cs b/NEG/UI/UnityUi/Buttons/OpenWindow.cs index 2116d4e..701644b 100644 --- a/NEG/UI/UnityUi/Buttons/OpenWindow.cs +++ b/NEG/UI/UnityUi/Buttons/OpenWindow.cs @@ -1,4 +1,6 @@ -using System; +using NEG.UI.UnityUi.Window; +using NEG.UI.UnityUi.WindowSlot; +using System; using UnityEngine; using NEG.UI.Window; using NEG.UI.WindowSlot; diff --git a/NEG/UI/UnityUi/CarouselList.meta b/NEG/UI/UnityUi/CarouselList.meta new file mode 100644 index 0000000..0f885c8 --- /dev/null +++ b/NEG/UI/UnityUi/CarouselList.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d6bf45a37eca4dc4aa1926eb7dce5ab6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NEG/UI/UnityUi/CarouselList/CarouselList.cs b/NEG/UI/UnityUi/CarouselList/CarouselList.cs new file mode 100644 index 0000000..e9e08b9 --- /dev/null +++ b/NEG/UI/UnityUi/CarouselList/CarouselList.cs @@ -0,0 +1,71 @@ +using NEG.UI.UnityUi.Buttons; +using System.Collections.Generic; +using TMPro; +using UnityEngine; + + +namespace NEG.UI.UnityUi +{ + public class CarouselList : MonoBehaviour + { + public string CurrentOption { get; private set; } + public int CurrentOptionId { get; private set; } + + [SerializeField] private BaseButton nextButton; + [SerializeField] private BaseButton prevButton; + [SerializeField] private TMP_Text currentOptionText; + + private List options; + + public void SetOptions(List options) + { + this.options = options; + SelectOption(0); + } + + public void SelectNextOption() => ChangeOption(true); + public void SelectPrevOption() => ChangeOption(false); + + public void SelectOption(int option) + { + if (option < 0 || option >= options.Count) + { + Debug.LogError("Invalid option number"); + return; + } + CurrentOptionId = option; + CurrentOption = options[option]; + currentOptionText.text = CurrentOption; + } + + /// + /// Select option with provided value. Use with caution, better use + /// + /// option value to select + public void SelectOption(string option) + { + if (options.Count == 0) + { + Debug.LogError("Carousel List cannot be empty when selecting option"); + return; + } + + int index = options.IndexOf(option); + if (index == -1) + { + Debug.LogError($"Option {option} not found"); + return; + } + + SelectOption(index); + } + + private void Awake() + { + nextButton.OnButtonPressed += SelectNextOption; + prevButton.OnButtonPressed += SelectPrevOption; + } + + private void ChangeOption(bool next) => SelectOption((CurrentOptionId + (next ? 1 : -1) + options.Count) % options.Count); + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/CarouselList/CarouselList.cs.meta b/NEG/UI/UnityUi/CarouselList/CarouselList.cs.meta new file mode 100644 index 0000000..afd4c30 --- /dev/null +++ b/NEG/UI/UnityUi/CarouselList/CarouselList.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9915ee4d467e47e485c4a48be295b5c2 +timeCreated: 1672834830 \ No newline at end of file diff --git a/NEG/UI/UnityUi/MonoUiManager.cs b/NEG/UI/UnityUi/MonoUiManager.cs new file mode 100644 index 0000000..e74d6e8 --- /dev/null +++ b/NEG/UI/UnityUi/MonoUiManager.cs @@ -0,0 +1,49 @@ +using NEG.UI.Area; +using NEG.UI.Popup; +using NEG.UI.UnityUi.Popup; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Assertions; +using UnityEngine.SceneManagement; + +namespace NEG.UI.UnityUi +{ + /// + /// Implements ui using UnityUI and Unity Event System with New Input System. + /// You have to provide prefabs with addresses: + /// - NEG/UI/PopupCanvas - prefab with canvas to create popups (will be created on every scene) + /// - NEG/UI/DefaultPopupPrefab - prefab of default popup with 2 options (has to have component) + /// + public class MonoUiManager : UiManager + { + private readonly MonoDefaultPopup defaultPopupPrefab; + private readonly GameObject canvasPrefab; + + public MonoUiManager(IArea startArea) : base(startArea) + { + var prefabs = + Addressables.LoadAssetsAsync(new List() { "NEG/UI/PopupCanvas", "NEG/UI/DefaultPopupPrefab" }, (_) => { }, Addressables.MergeMode.Union).WaitForCompletion(); + + Assert.AreEqual(prefabs.Count, 2, "No prefabs was provided. Please check MonoUiManager class documentation"); + Assert.IsNotNull(prefabs[0].GetComponent()); + Assert.IsNotNull(prefabs[1].GetComponent()); + + canvasPrefab = prefabs[0]; + defaultPopupPrefab = prefabs[1].GetComponent(); + + SpawnDefaultPopup(); + + SceneManager.activeSceneChanged += (_, _) => SpawnDefaultPopup(); + } + + private void SpawnDefaultPopup() + { + var canvas = Object.Instantiate(canvasPrefab); + canvas.name = "DefaultPopupCanvas"; + SetDefaultPopup(Object.Instantiate(defaultPopupPrefab, canvas.transform)); + currentDefaultPopup.Close(true); + } + + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/MonoUiManager.cs.meta b/NEG/UI/UnityUi/MonoUiManager.cs.meta new file mode 100644 index 0000000..635c484 --- /dev/null +++ b/NEG/UI/UnityUi/MonoUiManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: db04ec3144594df1bf4739e142d1b064 +timeCreated: 1672155707 \ No newline at end of file diff --git a/NEG/UI/UnityUi/NEG.UI.UnityUi.asmdef b/NEG/UI/UnityUi/NEG.UI.UnityUi.asmdef index b6668f6..833fde9 100644 --- a/NEG/UI/UnityUi/NEG.UI.UnityUi.asmdef +++ b/NEG/UI/UnityUi/NEG.UI.UnityUi.asmdef @@ -5,7 +5,9 @@ "GUID:343deaaf83e0cee4ca978e7df0b80d21", "GUID:7361f1d9c43da6649923760766194746", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:0c752da273b17c547ae705acf0f2adf2" + "GUID:0c752da273b17c547ae705acf0f2adf2", + "GUID:9e24947de15b9834991c9d8411ea37cf", + "GUID:84651a3751eca9349aac36a66bba901b" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/NEG/UI/UnityUi/Popup.meta b/NEG/UI/UnityUi/Popup.meta new file mode 100644 index 0000000..d38ae69 --- /dev/null +++ b/NEG/UI/UnityUi/Popup.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 31e9b0c7a4425a248841ec2f02b92157 +timeCreated: 1670707809 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs b/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs new file mode 100644 index 0000000..e265243 --- /dev/null +++ b/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs @@ -0,0 +1,36 @@ +using NEG.UI.Popup; +using NEG.UI.UnityUi.Buttons; +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; + +namespace NEG.UI.UnityUi.Popup +{ + public class MonoDefaultPopup : MonoPopup, IDefaultPopup + { + [SerializeField] private TMP_Text titleText; + [SerializeField] private TMP_Text contentText; + [SerializeField] private Transform buttonsParent; + [SerializeField] private BaseButton buttonPrefab; + + public void SetContent(string title, string content, List<(string, Action)> options) + { + foreach (Transform child in buttonsParent) + { + Destroy(child.gameObject); + } + + titleText.text = title; + contentText.text = content; + + foreach ((string text, Action action) item in options) + { + var button = Instantiate(buttonPrefab, buttonsParent); + button.SetText(item.text); + button.OnButtonPressed += item.action; + button.OnButtonPressed += () => Close(); + } + } + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs.meta b/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs.meta new file mode 100644 index 0000000..24e8172 --- /dev/null +++ b/NEG/UI/UnityUi/Popup/MonoDefaultPopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 831fb29e6c6d4997a7d471c646eec077 +timeCreated: 1672154589 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Popup/MonoPopup.cs b/NEG/UI/UnityUi/Popup/MonoPopup.cs new file mode 100644 index 0000000..5d4f9eb --- /dev/null +++ b/NEG/UI/UnityUi/Popup/MonoPopup.cs @@ -0,0 +1,28 @@ +using NEG.UI.Popup; +using System; +using UnityEngine; + +namespace NEG.UI.UnityUi.Popup +{ + public class MonoPopup : MonoBehaviour, IPopup + { + protected PopupData data; + + public void Show(PopupData data) + { + this.data = data; + gameObject.SetActive(true); + } + + public void Close(bool silence = false) + { + gameObject.SetActive(false); + + if(silence) + return; + + UiManager.Instance.PopupClosed(data); + } + + } +} \ No newline at end of file diff --git a/NEG/UI/Popup/MonoPopup.cs.meta b/NEG/UI/UnityUi/Popup/MonoPopup.cs.meta similarity index 54% rename from NEG/UI/Popup/MonoPopup.cs.meta rename to NEG/UI/UnityUi/Popup/MonoPopup.cs.meta index 292cb60..604f195 100644 --- a/NEG/UI/Popup/MonoPopup.cs.meta +++ b/NEG/UI/UnityUi/Popup/MonoPopup.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: d3057d16be014bd58847b118a24c21dd +guid: 5d3466b2fe771184eae1ca72c1006d0c timeCreated: 1670707831 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Window.meta b/NEG/UI/UnityUi/Window.meta new file mode 100644 index 0000000..12ce3d8 --- /dev/null +++ b/NEG/UI/UnityUi/Window.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 65adacad835c2cf4b9062296be6098d2 +timeCreated: 1670707788 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Window/MonoWindow.cs b/NEG/UI/UnityUi/Window/MonoWindow.cs new file mode 100644 index 0000000..36f5d7e --- /dev/null +++ b/NEG/UI/UnityUi/Window/MonoWindow.cs @@ -0,0 +1,48 @@ +using NEG.UI.Area; +using NEG.UI.UnityUi.WindowSlot; +using NEG.UI.Window; +using NEG.UI.WindowSlot; +using System; +using UnityEngine; + +namespace NEG.UI.UnityUi.Window +{ + public class MonoWindow : MonoBehaviour, IWindow + { + public IWindowSlot Parent { get; private set; } + public IWindowSlot ChildWindowSlot => childWindowArea; + + [SerializeField] private MonoWindowSlot childWindowArea; + [SerializeField] private WindowController controller; + + void IWindow.SetOpenedState(IWindowSlot parentSlot) + { + gameObject.SetActive(true); + Parent = parentSlot; + if (controller != null) + controller.OnOpened(); + } + + public void SetData(object data) + { + if (controller != null) + controller.SetData(data); + } + + void IWindow.SetClosedState() + { + gameObject.SetActive(false); + Parent = null; + if(childWindowArea != null) + ChildWindowSlot.CloseAllWindows(); + } + + private void Awake() => ((IWindow)this).SetClosedState(); + + private void OnValidate() + { + if (controller == null) + controller = GetComponent(); + } + } +} \ No newline at end of file diff --git a/NEG/UI/Window/MonoWindow.cs.meta b/NEG/UI/UnityUi/Window/MonoWindow.cs.meta similarity index 54% rename from NEG/UI/Window/MonoWindow.cs.meta rename to NEG/UI/UnityUi/Window/MonoWindow.cs.meta index 1b65326..4f6091a 100644 --- a/NEG/UI/Window/MonoWindow.cs.meta +++ b/NEG/UI/UnityUi/Window/MonoWindow.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 3807b2039b1941d999642e241ce1f463 +guid: 85d136d6850728d4b96c26fa286ffe3c timeCreated: 1670709296 \ No newline at end of file diff --git a/NEG/UI/UnityUi/Window/WindowController.cs b/NEG/UI/UnityUi/Window/WindowController.cs new file mode 100644 index 0000000..8e07abb --- /dev/null +++ b/NEG/UI/UnityUi/Window/WindowController.cs @@ -0,0 +1,18 @@ +using System; +using UnityEngine; + +namespace NEG.UI.UnityUi.Window +{ + [RequireComponent(typeof(MonoWindow))] + //Due to prefab variants we need this + public abstract class WindowController : MonoBehaviour + { + protected MonoWindow window; + + public abstract void SetData(object data); + + public abstract void OnOpened(); + + protected virtual void Awake() => window = GetComponent(); + } +} \ No newline at end of file diff --git a/NEG/UI/UnityUi/Window/WindowController.cs.meta b/NEG/UI/UnityUi/Window/WindowController.cs.meta new file mode 100644 index 0000000..ea8cd4c --- /dev/null +++ b/NEG/UI/UnityUi/Window/WindowController.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9e3614c1d2b94294937351b640139829 +timeCreated: 1672766736 \ No newline at end of file diff --git a/NEG/UI/UnityUi/WindowSlot.meta b/NEG/UI/UnityUi/WindowSlot.meta new file mode 100644 index 0000000..6065ce9 --- /dev/null +++ b/NEG/UI/UnityUi/WindowSlot.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 47e385f6fb552d74ab4cf25ef6c76595 +timeCreated: 1670707801 \ No newline at end of file diff --git a/NEG/UI/WindowSlot/MonoWindowSlot.cs b/NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs similarity index 85% rename from NEG/UI/WindowSlot/MonoWindowSlot.cs rename to NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs index a5e581b..5d454e4 100644 --- a/NEG/UI/WindowSlot/MonoWindowSlot.cs +++ b/NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs @@ -1,8 +1,9 @@ using NEG.UI.Area; using NEG.UI.Window; +using NEG.UI.WindowSlot; using UnityEngine; -namespace NEG.UI.WindowSlot +namespace NEG.UI.UnityUi.WindowSlot { public abstract class MonoWindowSlot : MonoBehaviour, IWindowSlot { diff --git a/NEG/UI/WindowSlot/MonoWindowSlot.cs.meta b/NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs.meta similarity index 54% rename from NEG/UI/WindowSlot/MonoWindowSlot.cs.meta rename to NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs.meta index cc53140..c91edeb 100644 --- a/NEG/UI/WindowSlot/MonoWindowSlot.cs.meta +++ b/NEG/UI/UnityUi/WindowSlot/MonoWindowSlot.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 7bc46ac7f2454eaa934561b7ef20ff88 +guid: e5a19b184ce9338449ad6ba899a26cda timeCreated: 1670709404 \ No newline at end of file diff --git a/NEG/UI/WindowSlot/SingleWindowSlot.cs b/NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs similarity index 78% rename from NEG/UI/WindowSlot/SingleWindowSlot.cs rename to NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs index 8c88187..b1b7841 100644 --- a/NEG/UI/WindowSlot/SingleWindowSlot.cs +++ b/NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs @@ -1,4 +1,5 @@ -using NEG.UI.Window; +using NEG.UI.UnityUi.WindowSlot; +using NEG.UI.Window; using UnityEngine; namespace NEG.UI.WindowSlot @@ -10,9 +11,9 @@ namespace NEG.UI.WindowSlot get => currentWindow; set { - currentWindow?.Close(); + currentWindow?.SetClosedState(); currentWindow = value; - currentWindow?.Open(this); + currentWindow?.SetOpenedState(this); } } diff --git a/NEG/UI/WindowSlot/SingleWindowSlot.cs.meta b/NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs.meta similarity index 54% rename from NEG/UI/WindowSlot/SingleWindowSlot.cs.meta rename to NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs.meta index e8f6309..5209494 100644 --- a/NEG/UI/WindowSlot/SingleWindowSlot.cs.meta +++ b/NEG/UI/UnityUi/WindowSlot/SingleWindowSlot.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 9358679df24f458f8836506344eb169d +guid: 79c0738c63b2b5c40b96acd89441f78f timeCreated: 1670716632 \ No newline at end of file diff --git a/NEG/UI/Window/IWindow.cs b/NEG/UI/Window/IWindow.cs index 157ae64..02961b4 100644 --- a/NEG/UI/Window/IWindow.cs +++ b/NEG/UI/Window/IWindow.cs @@ -1,4 +1,5 @@ -using NEG.UI.Area; +using JetBrains.Annotations; +using NEG.UI.Area; using NEG.UI.WindowSlot; using UnityEngine; @@ -7,34 +8,39 @@ namespace NEG.UI.Window public interface IWindow { IWindowSlot Parent { get; } - IArea ChildWindowArea { get; } + IWindowSlot ChildWindowSlot { get; } /// - /// Call internally by slot to open window + /// Called internally by slot to open window. /// - /// Slot that opens window - internal void Open(IWindowSlot parentSlot); + /// slot that opens window + void SetOpenedState(IWindowSlot parentSlot); + + void SetData(object data); /// - /// Call internally to close window by slot + /// Called internally to close window by slot. /// - internal void Close(); + void SetClosedState(); } public static class WindowInterfaceExtensions { //Open - public static void Open(this IWindow window, IWindowSlot slot = null) + public static void Open(this IWindow window, IWindowSlot slot = null, object data = null) { - if(slot != null) + if (slot != null) + { slot.AttachWindow(window); + window.SetData(data); + } else - UiManager.Instance.CurrentArea.OpenWindow(window); + UiManager.Instance.CurrentArea.OpenWindow(window, data); } - public static void Open(this IWindow window, IArea area) => area.OpenWindow(window); - public static void OpenChildWindow(this IWindow window, IWindow windowToOpen, IWindowSlot slot = null) + public static void Open(this IWindow window, IArea area, object data = null) => area.OpenWindow(window, data); + public static void OpenChildWindow(this IWindow window, IWindow windowToOpen, object data = null) { - if (window.ChildWindowArea == null) + if (window.ChildWindowSlot == null) { Debug.LogError("This window doesn't contain area for child windows"); return; @@ -44,5 +50,6 @@ namespace NEG.UI.Window } public static void Close(this IWindow window) => window.Parent.DetachWindow(window); + } } \ No newline at end of file diff --git a/NEG/UI/Window/MonoWindow.cs b/NEG/UI/Window/MonoWindow.cs deleted file mode 100644 index 7f443e8..0000000 --- a/NEG/UI/Window/MonoWindow.cs +++ /dev/null @@ -1,30 +0,0 @@ -using NEG.UI.Area; -using NEG.UI.Popup; -using NEG.UI.WindowSlot; -using System; -using UnityEngine; - -namespace NEG.UI.Window -{ - public class MonoWindow : MonoBehaviour, IWindow - { - public IWindowSlot Parent { get; private set; } - public IArea ChildWindowArea => childWindowArea; - - [SerializeField] private MonoArea childWindowArea; - - void IWindow.Open(IWindowSlot parentSlot) - { - gameObject.SetActive(true); - Parent = parentSlot; - } - - void IWindow.Close() - { - gameObject.SetActive(false); - Parent = null; - } - - private void Awake() => ((IWindow)this).Close(); - } -} \ No newline at end of file