using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MLEM.Extensions; using MLEM.Input; using MLEM.Misc; using MLEM.Textures; using MLEM.Ui.Style; using SoundEffectInfo = MLEM.Sound.SoundEffectInfo; namespace MLEM.Ui.Elements { /// /// This class represents a generic base class for ui elements of a . /// public abstract class Element : GenericDataHolder, IDisposable { /// /// This field holds an epsilon value used in element , position and resulting calculations to mitigate floating point rounding inaccuracies. /// If ui elements used are extremely small or extremely large, this value can be reduced or increased. /// public static float Epsilon = 0.01F; /// /// The ui system that this element is currently a part of /// public UiSystem System { get => this.system; internal set { this.system = value; this.Controls = value?.Controls; this.Style = value?.Style; } } /// /// This Element's current . /// When this value is changed, is called. /// Note that this value is automatically set to the 's ui style when it is changed. /// public UiStyle Style { get => this.style; set { if (this.style != value) { this.style = value; if (value != null) this.InitStyle(value); } } } /// /// The controls that this element's uses /// public UiControls Controls; /// /// This element's parent element. /// If this element has no parent (it is the of a ui system), this value is null. /// public Element Parent { get; private set; } /// /// This element's . /// Note that this value is set even if this element has a . To get the element that represents the root element, use . /// public RootElement Root { get; internal set; } /// /// The scale that this ui element renders with /// public float Scale => this.Root.ActualScale; /// /// The that this element uses for positioning within its parent /// public Anchor Anchor { get => this.anchor; set { if (this.anchor == value) return; this.anchor = value; this.SetAreaDirty(); } } /// /// The size of this element, where X represents the width and Y represents the height. /// If the x or y value of the size is between 0 and 1, the size will be seen as a percentage of its parent's size rather than as an absolute value. /// If the x (or y) value of the size is negative, the width (or height) is seen as a percentage of the element's resulting height (or width). /// /// /// The following example combines both types of percentage-based sizing. /// If this element is inside of a whose width is 20, this element's width will be set to 0.5 * 20 = 10, and its height will be set to 2.5 * 10 = 25. /// /// element.Size = new Vector2(0.5F, -2.5F); /// /// public Vector2 Size { get => this.size; set { if (this.size == value) return; this.size = value; this.SetAreaDirty(); } } /// /// The , but with applied. /// public Vector2 ScaledSize => this.size * this.Scale; /// /// This element's offset from its default position, which is dictated by its . /// Note that, depending on the side that the element is anchored to, this offset moves it in a different direction. /// public Vector2 PositionOffset { get => this.offset; set { if (this.offset == value) return; this.offset = value; this.SetAreaDirty(); } } /// /// The , but with applied. /// public Vector2 ScaledOffset => this.offset * this.Scale; /// /// The , but with applied. /// public Padding ScaledPadding => this.Padding.Value * this.Scale; /// /// The , but with applied. /// public Padding ScaledChildPadding => this.ChildPadding.Value * this.Scale; /// /// This element's current , but with applied. /// public RectangleF ChildPaddedArea => this.UnscrolledArea.Shrink(this.ScaledChildPadding); /// /// This element's area, without respecting its . /// This area is updated automatically to fit this element's sizing and positioning properties. /// public RectangleF UnscrolledArea { get { this.UpdateAreaIfDirty(); return this.area; } } /// /// The of this element, but with applied. /// public RectangleF Area => this.UnscrolledArea.OffsetCopy(this.ScaledScrollOffset); /// /// The area that this element is displayed in, which is shrunk by this element's . /// This is the property that should be used for drawing this element, as well as mouse input handling and culling. /// public RectangleF DisplayArea => this.Area.Shrink(this.ScaledPadding); /// /// The offset that this element has as a result of scrolling. /// public Vector2 ScrollOffset; /// /// The , but with applied. /// public Vector2 ScaledScrollOffset => this.ScrollOffset * this.Scale; /// /// Set this property to true to cause this element to be hidden. /// Hidden elements don't receive input events, aren't rendered and don't factor into auto-anchoring. /// public bool IsHidden { get => this.isHidden; set { if (this.isHidden == value) return; this.isHidden = value; this.SetAreaDirty(); } } /// /// The priority of this element as part of its element. /// A higher priority means the element will be drawn first and, if auto-anchoring is used, anchored higher up within its parent. /// public int Priority { get => this.priority; set { if (this.priority == value) return; this.priority = value; if (this.Parent != null) this.Parent.SetSortedChildrenDirty(); } } /// /// This element's transform matrix. /// Can easily be scaled using . /// Note that, when this is non-null, a new call is used for this element. /// public Matrix Transform = Matrix.Identity; /// /// The call that this element should make to to begin drawing. /// Note that, when this is non-null, a new call is used for this element. /// public BeginDelegate BeginImpl; /// /// Set this field to false to disallow the element from being selected. /// An unselectable element is skipped by automatic navigation and its callback will never be called. /// public bool CanBeSelected = true; /// /// Set this field to false to disallow the element from reacting to being moused over. /// public bool CanBeMoused = true; /// /// Set this field to false to disallow this element's and events to be called. /// public bool CanBePressed = true; /// /// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point. /// public bool CanAutoAnchorsAttach = true; /// /// Set this field to true to cause this element's width to be automatically calculated based on the area that its take up. /// To use this element's 's X coordinate as a minimum or maximum width rather than ignoring it, set or to true. /// public bool SetWidthBasedOnChildren; /// /// Set this field to true to cause this element's height to be automatically calculated based on the area that its take up. /// To use this element's 's Y coordinate as a minimum or maximum height rather than ignoring it, set or to true. /// public bool SetHeightBasedOnChildren; /// /// If this field is set to true, and or are enabled, the resulting width or height will always be greather than or equal to this element's . /// For example, if an element's 's Y coordinate is set to 20, but there is only one child with a height of 10 in it, the element's height would be shrunk to 10 if this value was false, but would remain at 20 if it was true. /// Note that this value only has an effect if or are enabled. /// public bool TreatSizeAsMinimum; /// /// If this field is set to true, and or are enabled, the resulting width or height weill always be less than or equal to this element's . /// Note that this value only has an effect if or are enabled. /// public bool TreatSizeAsMaximum; /// /// Set this field to true to cause this element's final display area to never exceed that of its . /// If the resulting area is too large, the size of this element is shrunk to fit the target area. /// This can be useful if an element should fill the remaining area of a parent exactly. /// When setting this value after this element has already been added to a ui, should be called. /// public bool PreventParentSpill; /// /// Set this field to true to cause this element's final display ever to never overlap with any of its siblings (). /// If the resulting area is too large, the size of this element is shrunk to best accomodate for the areas of its siblings. /// When setting this value after this element has already been added to a ui, should be called. /// public bool PreventSiblingSpill; /// /// The transparency (alpha value) that this element is rendered with. /// Note that, when is called, this alpha value is multiplied with the 's alpha value and passed down to this element's . /// public float DrawAlpha = 1; /// /// Stores whether this element is currently being moused over or touched. /// public bool IsMouseOver { get; protected set; } /// /// Stores whether this element is its 's . /// public bool IsSelected { get; protected set; } /// /// A style property that contains the selection indicator that is displayed on this element if it is the /// public StyleProp SelectionIndicator; /// /// A style property that contains the sound effect that is played when this element's is called /// public StyleProp ActionSound; /// /// A style property that contains the sound effect that is played when this element's is called /// public StyleProp SecondActionSound; /// /// The padding that this element has. /// The padding is subtracted from the element's , and it is an area that the element does not extend into. This means that this element's resulting does not include this padding. /// public StyleProp Padding; /// /// The child padding that this element has. /// The child padding moves any added to this element inwards by the given amount in each direction. /// When setting this style after this element has already been added to a ui, should be called. /// public StyleProp ChildPadding; /// /// Event that is called after this element is drawn, but before its children are drawn /// public DrawCallback OnDrawn; /// /// Event that is called when this element is updated /// public TimeCallback OnUpdated; /// /// Event that is called when this element is pressed /// public GenericCallback OnPressed; /// /// Event that is called when this element is pressed using the secondary action /// public GenericCallback OnSecondaryPressed; /// /// Event that is called when this element's is turned true /// public GenericCallback OnSelected; /// /// Event that is called when this element's is turned false /// public GenericCallback OnDeselected; /// /// Event that is called when this element starts being moused over /// public GenericCallback OnMouseEnter; /// /// Event that is called when this element stops being moused over /// public GenericCallback OnMouseExit; /// /// Event that is called when this element starts being touched /// public GenericCallback OnTouchEnter; /// /// Event that is called when this element stops being touched /// public GenericCallback OnTouchExit; /// /// Event that is called when text input is made. /// When an element uses this event, it should call on construction to ensure that the MLEM platform is initialized. /// Note that this event is called for every element, even if it is not selected. /// Also note that if the active uses an on-screen keyboard, this event is never called. /// public TextInputCallback OnTextInput; /// /// Event that is called when this element's is changed. /// public GenericCallback OnAreaUpdated; /// /// Event that is called when this element's area is marked as dirty using . /// public GenericCallback OnAreaDirty; /// /// Event that is called when the element that is currently being moused changes within the ui system. /// Note that the event fired doesn't necessarily correlate to this specific element. /// public OtherElementCallback OnMousedElementChanged; /// /// Event that is called when the element that is currently being touched changes within the ui system. /// Note that the event fired doesn't necessarily correlate to this specific element. /// public OtherElementCallback OnTouchedElementChanged; /// /// Event that is called when the element that is currently selected changes within the ui system. /// Note that the event fired doesn't necessarily correlate to this specific element. /// public OtherElementCallback OnSelectedElementChanged; /// /// Event that is called when the next element to select when pressing tab is calculated. /// To cause a different element than the default one to be selected, return it during this event. /// public TabNextElementCallback GetTabNextElement; /// /// Event that is called when the next element to select when using gamepad input is calculated. /// To cause a different element than the default one to be selected, return it during this event. /// public GamepadNextElementCallback GetGamepadNextElement; /// /// Event that is called when a child is added to this element using /// public OtherElementCallback OnChildAdded; /// /// Event that is called when a child is removed from this element using /// public OtherElementCallback OnChildRemoved; /// /// Event that is called when this element's method is called, which also happens in . /// This event is useful for unregistering global event handlers when this object should be destroyed. /// public GenericCallback OnDisposed; /// /// A list of all of this element's direct children. /// Use or to manipulate this list while calling all of the necessary callbacks. /// protected readonly IList Children; /// /// A sorted version of . The children are sorted by their . /// protected IList SortedChildren { get { this.UpdateSortedChildrenIfDirty(); return this.sortedChildren; } } /// /// The input handler that this element's use /// protected InputHandler Input => this.Controls.Input; private readonly List children = new List(); private bool sortedChildrenDirty; private IList sortedChildren; private UiSystem system; private Anchor anchor; private Vector2 size; private Vector2 offset; private RectangleF area; private bool areaDirty; private bool isHidden; private int priority; private UiStyle style; /// /// Creates a new element with the given anchor and size and sets up some default event reactions. /// /// This element's /// This element's default protected Element(Anchor anchor, Vector2 size) { this.anchor = anchor; this.size = size; this.Children = new ReadOnlyCollection(this.children); this.OnMouseEnter += element => this.IsMouseOver = true; this.OnMouseExit += element => this.IsMouseOver = false; this.OnTouchEnter += element => this.IsMouseOver = true; this.OnTouchExit += element => this.IsMouseOver = false; this.OnSelected += element => this.IsSelected = true; this.OnDeselected += element => this.IsSelected = false; this.GetTabNextElement += (backward, next) => next; this.GetGamepadNextElement += (dir, next) => next; this.SetAreaDirty(); this.SetSortedChildrenDirty(); } /// ~Element() { this.Dispose(); } /// /// Adds a child to this element. /// /// The child element to add /// The index to add the child at, or -1 to add it to the end of the list /// The type of child to add /// This element, for chaining public virtual T AddChild(T element, int index = -1) where T : Element { if (index < 0 || index > this.children.Count) index = this.children.Count; this.children.Insert(index, element); element.Parent = this; element.AndChildren(e => { e.Root = this.Root; e.System = this.System; this.Root?.InvokeOnElementAdded(e); this.OnChildAdded?.Invoke(this, e); }); this.SetSortedChildrenDirty(); element.SetAreaDirty(); return element; } /// /// Removes the given child from this element. /// /// The child element to remove public virtual void RemoveChild(Element element) { this.children.Remove(element); // set area dirty here so that a dirty call is made // upwards to us if the element is auto-positioned element.SetAreaDirty(); element.Parent = null; element.AndChildren(e => { e.Root = null; e.System = null; this.Root?.InvokeOnElementRemoved(e); this.OnChildRemoved?.Invoke(this, e); }); this.SetSortedChildrenDirty(); } /// /// Removes all children from this element that match the given condition. /// /// The condition that determines if a child should be removed public virtual void RemoveChildren(Func condition = null) { for (var i = this.Children.Count - 1; i >= 0; i--) { var child = this.Children[i]; if (condition == null || condition(child)) { this.RemoveChild(child); } } } /// /// Causes to be recalculated as soon as possible. /// public void SetSortedChildrenDirty() { this.sortedChildrenDirty = true; } /// /// Updates the list if is true. /// public void UpdateSortedChildrenIfDirty() { if (this.sortedChildrenDirty) this.ForceUpdateSortedChildren(); } /// /// Forces an update of the list. /// public virtual void ForceUpdateSortedChildren() { this.sortedChildrenDirty = false; this.sortedChildren = new ReadOnlyCollection(this.Children.OrderBy(e => e.Priority).ToArray()); } /// /// Causes this element's to be recalculated as soon as possible. /// If this element is auto-anchored or its parent automatically changes its size based on its children, this element's parent's area is also marked dirty. /// public void SetAreaDirty() { this.areaDirty = true; if (this.Parent != null) { // set parent dirty if the parent's layout depends on our area if (this.Anchor >= Anchor.AutoLeft || this.Parent.SetWidthBasedOnChildren || this.Parent.SetHeightBasedOnChildren) this.Parent.SetAreaDirty(); // set siblings dirty that depend on our area foreach (var sibling in this.GetSiblings()) { if (sibling.PreventSiblingSpill) sibling.SetAreaDirty(); } } this.System?.InvokeOnElementAreaDirty(this); } /// /// Updates this element's and all of its by calling if is true. /// /// Whether was true and was called public bool UpdateAreaIfDirty() { if (this.areaDirty) { this.ForceUpdateArea(); return true; } return false; } /// /// Forces this element's to be updated if it is not . /// This method also updates all of this element's 's areas. /// public virtual void ForceUpdateArea() { this.areaDirty = false; if (this.IsHidden) return; // if the parent's area is dirty, it would get updated anyway when querying its ChildPaddedArea, // which would cause our ForceUpdateArea code to be run twice, so we only update our parent instead if (this.Parent != null && this.Parent.UpdateAreaIfDirty()) return; var parentArea = this.Parent != null ? this.Parent.ChildPaddedArea : (RectangleF) this.system.Viewport; var parentCenterX = parentArea.X + parentArea.Width / 2; var parentCenterY = parentArea.Y + parentArea.Height / 2; var actualSize = this.CalcActualSize(parentArea); var recursion = 0; UpdateDisplayArea(actualSize); void UpdateDisplayArea(Vector2 newSize) { var pos = new Vector2(); switch (this.anchor) { case Anchor.TopLeft: case Anchor.AutoLeft: case Anchor.AutoInline: case Anchor.AutoInlineIgnoreOverflow: pos.X = parentArea.X + this.ScaledOffset.X; pos.Y = parentArea.Y + this.ScaledOffset.Y; break; case Anchor.TopCenter: case Anchor.AutoCenter: pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X; pos.Y = parentArea.Y + this.ScaledOffset.Y; break; case Anchor.TopRight: case Anchor.AutoRight: pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X; pos.Y = parentArea.Y + this.ScaledOffset.Y; break; case Anchor.CenterLeft: pos.X = parentArea.X + this.ScaledOffset.X; pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y; break; case Anchor.Center: pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X; pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y; break; case Anchor.CenterRight: pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X; pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y; break; case Anchor.BottomLeft: pos.X = parentArea.X + this.ScaledOffset.X; pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y; break; case Anchor.BottomCenter: pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X; pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y; break; case Anchor.BottomRight: pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X; pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y; break; } if (this.Anchor >= Anchor.AutoLeft) { Element previousChild; if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) { previousChild = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); } else { previousChild = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); } if (previousChild != null) { var prevArea = previousChild.GetAreaForAutoAnchors(); switch (this.Anchor) { case Anchor.AutoLeft: case Anchor.AutoCenter: case Anchor.AutoRight: pos.Y = prevArea.Bottom + this.ScaledOffset.Y; break; case Anchor.AutoInline: var newX = prevArea.Right + this.ScaledOffset.X; // with awkward ui scale values, floating point rounding can cause an element that would usually // be positioned correctly to be pushed into the next line due to a very small deviation if (newX + newSize.X <= parentArea.Right + Epsilon) { pos.X = newX; pos.Y = prevArea.Y + this.ScaledOffset.Y; } else { pos.Y = prevArea.Bottom + this.ScaledOffset.Y; } break; case Anchor.AutoInlineIgnoreOverflow: pos.X = prevArea.Right + this.ScaledOffset.X; pos.Y = prevArea.Y + this.ScaledOffset.Y; break; } } } if (this.PreventParentSpill) { if (pos.X < parentArea.X) pos.X = parentArea.X; if (pos.Y < parentArea.Y) pos.Y = parentArea.Y; if (pos.X + newSize.X > parentArea.Right) newSize.X = parentArea.Right - pos.X; if (pos.Y + newSize.Y > parentArea.Bottom) newSize.Y = parentArea.Bottom - pos.Y; } if (this.PreventSiblingSpill) { foreach (var sibling in this.GetSiblings(e => !e.IsHidden)) { var leftIntersect = sibling.Area.Right - pos.X; var rightIntersect = pos.X + newSize.X - sibling.Area.Left; var bottomIntersect = sibling.Area.Bottom - pos.Y; var topIntersect = pos.Y + newSize.Y - sibling.Area.Top; if (leftIntersect > 0 && rightIntersect > 0 && bottomIntersect > 0 && topIntersect > 0) { if (rightIntersect + leftIntersect < topIntersect + bottomIntersect) { if (rightIntersect > leftIntersect) { pos.X = Math.Max(pos.X, sibling.Area.Right); } else { newSize.X = Math.Min(pos.X + newSize.X, sibling.Area.Left) - pos.X; } } else { if (topIntersect > bottomIntersect) { pos.Y = Math.Max(pos.Y, sibling.Area.Bottom); } else { newSize.Y = Math.Min(pos.Y + newSize.Y, sibling.Area.Top) - pos.Y; } } } } } this.area = new RectangleF(pos, newSize); this.System.InvokeOnElementAreaUpdated(this); foreach (var child in this.Children) child.ForceUpdateArea(); if (this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren) { Element foundChild = null; var autoSize = this.UnscrolledArea.Size; if (this.SetHeightBasedOnChildren) { var lowest = this.GetLowestChild(e => !e.IsHidden); if (lowest != null) { autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom; foundChild = lowest; } else { if (this.Children.Any(e => !e.IsHidden)) throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its height based on children but it only has visible children anchored too low ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})"); autoSize.Y = 0; } } if (this.SetWidthBasedOnChildren) { var rightmost = this.GetRightmostChild(e => !e.IsHidden); if (rightmost != null) { autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right; foundChild = rightmost; } else { if (this.Children.Any(e => !e.IsHidden)) throw new InvalidOperationException($"{this} with root {this.Root.Name} sets its width based on children but it only has visible children anchored too far right ({string.Join(", ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})"); autoSize.X = 0; } } if (this.TreatSizeAsMinimum) { autoSize = Vector2.Max(autoSize, actualSize); } else if (this.TreatSizeAsMaximum) { autoSize = Vector2.Min(autoSize, actualSize); } // we want to leave some leeway to prevent float rounding causing an infinite loop if (!autoSize.Equals(this.UnscrolledArea.Size, Epsilon)) { recursion++; if (recursion >= 16) { throw new ArithmeticException($"The area of {this} with root {this.Root.Name} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?"); } else { UpdateDisplayArea(autoSize); } } } } } /// /// Calculates the actual size that this element should take up, based on the area that its parent encompasses. /// By default, this is based on the information specified in 's documentation. /// /// This parent's area, or the ui system's viewport if it has no parent /// The actual size of this element, taking into account protected virtual Vector2 CalcActualSize(RectangleF parentArea) { var ret = new Vector2( this.size.X > 1 ? this.ScaledSize.X : parentArea.Width * this.size.X, this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y); if (this.size.X < 0) ret.X = -this.size.X * ret.Y; if (this.size.Y < 0) ret.Y = -this.size.Y * ret.X; return ret; } /// /// Returns the area that should be used for determining where auto-anchoring children should attach. /// /// The area for auto anchors protected virtual RectangleF GetAreaForAutoAnchors() { return this.UnscrolledArea; } /// /// Returns this element's lowest child element (in terms of y position) that matches the given condition. /// /// The condition to match /// The lowest element, or null if no such element exists public Element GetLowestChild(Func condition = null) { Element lowest = null; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; if (child.Anchor > Anchor.TopRight && child.Anchor < Anchor.AutoLeft) continue; if (lowest == null || child.UnscrolledArea.Bottom >= lowest.UnscrolledArea.Bottom) lowest = child; } return lowest; } /// /// Returns this element's rightmost child (in terms of x position) that matches the given condition. /// /// The condition to match /// The rightmost element, or null if no such element exists public Element GetRightmostChild(Func condition = null) { Element rightmost = null; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; if (child.Anchor < Anchor.AutoLeft && child.Anchor != Anchor.TopLeft && child.Anchor != Anchor.CenterLeft && child.Anchor != Anchor.BottomLeft) continue; if (rightmost == null || child.UnscrolledArea.Right >= rightmost.UnscrolledArea.Right) rightmost = child; } return rightmost; } /// /// Returns this element's lowest sibling that is also older (lower in its parent's list) than this element that also matches the given condition. /// The returned element's will always be equal to this element's . /// /// The condition to match /// The lowest older sibling of this element, or null if no such element exists public Element GetLowestOlderSibling(Func condition = null) { if (this.Parent == null) return null; Element lowest = null; foreach (var child in this.Parent.Children) { if (child == this) break; if (condition != null && !condition(child)) continue; if (lowest == null || child.UnscrolledArea.Bottom >= lowest.UnscrolledArea.Bottom) lowest = child; } return lowest; } /// /// Returns this element's first older sibling that matches the given condition. /// The returned element's will always be equal to this element's . /// /// The condition to match /// The older sibling, or null if no such element exists public Element GetOlderSibling(Func condition = null) { if (this.Parent == null) return null; Element older = null; foreach (var child in this.Parent.Children) { if (child == this) break; if (condition != null && !condition(child)) continue; older = child; } return older; } /// /// Returns all of this element's siblings that match the given condition. /// Siblings are elements that have the same as this element. /// /// The condition to match /// This element's siblings public IEnumerable GetSiblings(Func condition = null) { if (this.Parent == null) yield break; foreach (var child in this.Parent.Children) { if (condition != null && !condition(child)) continue; if (child != this) yield return child; } } /// /// Returns all of this element's children of the given type that match the given condition. /// Optionally, the entire tree of children (grandchildren) can be searched. /// /// The condition to match /// If this value is true, children of children of this element are also searched /// If this value is true, children for which the condition does not match will not have their children searched /// The type of children to search for /// All children that match the condition public IEnumerable GetChildren(Func condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) where T : Element { foreach (var child in this.Children) { var applies = child is T t && (condition == null || condition(t)); if (applies) yield return (T) child; if (regardGrandchildren && (!ignoreFalseGrandchildren || applies)) { foreach (var cc in child.GetChildren(condition, true, ignoreFalseGrandchildren)) yield return cc; } } } /// public IEnumerable GetChildren(Func condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) { return this.GetChildren(condition, regardGrandchildren, ignoreFalseGrandchildren); } /// /// Returns the parent tree of this element. /// The parent tree is this element's , followed by its parent, and so on, up until the 's . /// /// This element's parent tree public IEnumerable GetParentTree() { if (this.Parent == null) yield break; yield return this.Parent; foreach (var parent in this.Parent.GetParentTree()) yield return parent; } /// /// Returns a subset of that are currently relevant in terms of drawing and input querying. /// A only returns elements that are currently in view here. /// /// This element's relevant children protected virtual IList GetRelevantChildren() { return this.SortedChildren; } /// /// Updates this element and all of its /// /// The game's time public virtual void Update(GameTime time) { this.System.InvokeOnElementUpdated(this, time); foreach (var child in this.GetRelevantChildren()) if (child.System != null) child.Update(time); } /// /// Draws this element by calling internally. /// If or is set, a new call is also started. /// /// The game's time /// The sprite batch to use for drawing /// The alpha to draw this element and its children with /// The blend state that is used for drawing /// The sampler state that is used for drawing /// The effect that is used for drawing /// The depth stencil state that is used for drawing /// The transformation matrix that is used for drawing public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) { var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity; var mat = this.Transform * matrix; if (customDraw) { // end the usual draw so that we can begin our own batch.End(); // begin our own draw call if (this.BeginImpl != null) { this.BeginImpl(this, time, batch, alpha, blendState, samplerState, depthStencilState, effect, mat); } else { batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, mat); } } // draw content in custom begin call this.Draw(time, batch, alpha, blendState, samplerState, depthStencilState, effect, mat); if (customDraw) { // end our draw batch.End(); // begin the usual draw again for other elements batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix); } } /// /// Draws this element and all of its children. Override this method to draw the content of custom elements. /// Note that, when this is called, has already been called with custom etc. applied. /// /// The game's time /// The sprite batch to use for drawing /// The alpha to draw this element and its children with /// The blend state that is used for drawing /// The sampler state that is used for drawing /// The effect that is used for drawing /// The depth stencil state that is used for drawing /// The transformation matrix that is used for drawing public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) { this.System.InvokeOnElementDrawn(this, time, batch, alpha); if (this.IsSelected) this.System.InvokeOnSelectedElementDrawn(this, time, batch, alpha); foreach (var child in this.GetRelevantChildren()) { if (!child.IsHidden) child.DrawTransformed(time, batch, alpha * child.DrawAlpha, blendState, samplerState, depthStencilState, effect, matrix); } } /// /// Draws this element and all of its early. /// Drawing early involves drawing onto instances rather than onto the screen. /// Note that, when this is called, has not yet been called. /// /// The game's time /// The sprite batch to use for drawing /// The alpha to draw this element and its children with /// The blend state that is used for drawing /// The sampler state that is used for drawing /// The effect that is used for drawing /// The depth stencil state that is used for drawing /// The transformation matrix that is used for drawing public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) { foreach (var child in this.GetRelevantChildren()) { if (!child.IsHidden) child.DrawEarly(time, batch, alpha * child.DrawAlpha, blendState, samplerState, depthStencilState, effect, matrix); } } /// /// Returns the element under the given position, searching the current element and all of its . /// /// The position to query /// The element under the position, or null if no such element exists public virtual Element GetElementUnderPos(Vector2 position) { if (this.IsHidden) return null; position = this.TransformInverse(position); var relevant = this.GetRelevantChildren(); for (var i = relevant.Count - 1; i >= 0; i--) { var element = relevant[i].GetElementUnderPos(position); if (element != null) return element; } return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public virtual void Dispose() { this.OnDisposed?.Invoke(this); GC.SuppressFinalize(this); } /// /// Performs the specified action on this element and all of its /// /// The action to perform public void AndChildren(Action action) { action(this); foreach (var child in this.Children) child.AndChildren(action); } /// /// Sorts this element's using the given comparison. /// /// The comparison to sort by public void ReorderChildren(Comparison comparison) { this.children.Sort(comparison); } /// /// Reverses this element's list in the given range. /// /// The index to start reversing at /// The amount of elements to reverse public void ReverseChildren(int index = 0, int? count = null) { this.children.Reverse(index, count ?? this.Children.Count); } /// /// Scales this element's matrix based on the given scale and origin. /// /// The scale to use /// The origin to use for scaling, or null to use this element's center point public void ScaleTransform(float scale, Vector2? origin = null) { this.Transform = Matrix.CreateScale(scale, scale, 1) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.DisplayArea.Center), 0)); } /// /// Initializes this element's instances using the ui system's . /// /// The new style protected virtual void InitStyle(UiStyle style) { this.SelectionIndicator.SetFromStyle(style.SelectionIndicator); this.ActionSound.SetFromStyle(style.ActionSound); this.SecondActionSound.SetFromStyle(style.ActionSound); } /// /// Transforms the given by the inverse of this element's matrix. /// /// The position to transform /// The transformed position protected Vector2 TransformInverse(Vector2 position) { return this.Transform != Matrix.Identity ? Vector2.Transform(position, Matrix.Invert(this.Transform)) : position; } /// /// Transforms the given by this element's 's , the inverses of all of the matrices of this element's parent tree (), and the inverse of this element's matrix. /// Note that, when using , this operation is done recursively, which is more efficient. /// /// The position to transform /// The transformed position protected Vector2 TransformInverseAll(Vector2 position) { position = Vector2.Transform(position, this.Root.InvTransform); foreach (var parent in this.GetParentTree().Reverse()) position = parent.TransformInverse(position); return this.TransformInverse(position); } /// /// A delegate used for the event. /// /// The current element /// The key that was pressed /// The character that was input public delegate void TextInputCallback(Element element, Keys key, char character); /// /// A generic element-specific delegate. /// /// The current element public delegate void GenericCallback(Element element); /// /// A generic element-specific delegate that includes a second element. /// /// The current element /// The other element public delegate void OtherElementCallback(Element thisElement, Element otherElement); /// /// A delegate used inside of /// /// The element that is being drawn /// The game's time /// The sprite batch used for drawing /// The alpha this element is drawn with public delegate void DrawCallback(Element element, GameTime time, SpriteBatch batch, float alpha); /// /// A generic delegate used inside of /// /// The current element /// The game's time public delegate void TimeCallback(Element element, GameTime time); /// /// A delegate used by . /// /// If this value is true, is being held /// The element that is considered to be the next element by default public delegate Element TabNextElementCallback(bool backward, Element usualNext); /// /// A delegate used by . /// /// The direction of the gamepad button that was pressed /// The element that is considered to be the next element by default public delegate Element GamepadNextElementCallback(Direction2 dir, Element usualNext); /// /// A delegate method used for /// /// The custom draw group /// The game's time /// The sprite batch used for drawing /// This element's draw alpha /// The blend state used for drawing /// The sampler state used for drawing /// The effect used for drawing /// The depth stencil state used for drawing /// The transform matrix used for drawing public delegate void BeginDelegate(Element element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix); } }