using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MLEM.Extensions; using MLEM.Graphics; using MLEM.Input; using MLEM.Misc; using MLEM.Sound; using MLEM.Textures; using MLEM.Ui.Style; 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; private set { this.system = value; this.Controls = value?.Controls; this.Style = this.Style.OrStyle(value?.Style); } } /// /// 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; private 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). /// Additionally, if auto-sizing is used, can be set to add or subtract an absolute value from the automatically calculated size. /// /// /// The following example, ignoring , 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; /// /// If auto-sizing is used by setting less than or equal to 1, this property allows adding or subtracting an additional, absolute value from the automatically calculated size. /// If this element is not using auto-sizing, this property is ignored. /// /// /// Ignoring , if this element's is set to 0.5, 0.75 and its has a size of 200, 100, then an added absolute size of -50, 25 will result in a final size of 0.5 * 200 - 50, 0.75 * 100 + 25, or 50, 100. /// public Vector2 AutoSizeAddedAbsolute { get => this.autoSizeAddedAbsolute; set { if (this.autoSizeAddedAbsolute == value) return; this.autoSizeAddedAbsolute = value; this.SetAreaDirty(); } } /// /// The , but with applied. /// public Vector2 ScaledAutoSizeAddedAbsolute => this.autoSizeAddedAbsolute * 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 virtual 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, but not anchored higher up if auto-anchoring is used. /// 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 SpriteBatch.Begin 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 SpriteBatch.Begin call is used for this element. /// #pragma warning disable CS0618 [Obsolete("BeginImpl is deprecated. You can create a custom element class and override Draw instead.")] public BeginDelegate BeginImpl; #pragma warning restore CS0618 /// /// 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 virtual bool CanBeSelected { get => this.canBeSelected; set { this.canBeSelected = value; if (!this.canBeSelected && this.Root?.SelectedElement == this) this.Root.SelectElement(null); } } /// /// Set this field to false to disallow the element from reacting to being moused over. /// public virtual bool CanBeMoused { get; set; } = true; /// /// Set this field to false to disallow this element's and events to be called. /// public virtual bool CanBePressed { get; set; } = true; /// /// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point. /// public virtual bool CanAutoAnchorsAttach { get; set; } = 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 virtual bool SetWidthBasedOnChildren { get; set; } /// /// 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 virtual bool SetHeightBasedOnChildren { get; set; } /// /// 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 virtual bool TreatSizeAsMinimum { get; set; } /// /// 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 virtual bool TreatSizeAsMaximum { get; set; } /// /// 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. /// public virtual bool PreventParentSpill { get; set; } /// /// 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 virtual float DrawAlpha { get; set; } = 1; /// /// Stores whether this element is currently being moused over or touched. /// public bool IsMouseOver => this.Controls.MousedElement == this || this.Controls.TouchedElement == this; /// /// Returns whether this element is its 's . /// public bool IsSelected => this.Root.SelectedElement == this; /// /// Returns whether this element's method has been recently called and its area has not been updated since then using or . /// public bool AreaDirty { get; private set; } /// /// An optional string that represents a group of elements for automatic (keyboard and gamepad) navigation. /// All elements that share the same auto-nav group will be able to be navigated between, and all other elements will not be reachable from elements of other groups. /// Note that, if no element is previously selected and auto-navigation is invoked, this element cannot be chosen as the first element to navigate to if its auto-nav group is non-null. /// public virtual string AutoNavGroup { get; set; } /// /// This Element's current . /// When this property is set, is called. /// public StyleProp Style { get => this.style; set { this.style = value; if (this.style.HasValue()) this.InitStyle(this.style); } } /// /// 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. /// public StyleProp ChildPadding { get => this.childPadding; set { this.childPadding = value; this.SetAreaDirty(); } } /// /// 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 method is called while setting the . /// public GenericCallback OnStyleInit; /// /// 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 /// Note that, while this event is only called for immediate children of this element, is called for all children and grandchildren. /// public OtherElementCallback OnChildAdded; /// /// Event that is called when a child is removed from this element using . /// Note that, while this event is only called for immediate children of this element, is called for all children and grandchildren. /// public OtherElementCallback OnChildRemoved; /// /// Event that is called when this element is added to a , that is, when this element's is set to a non- value. /// public GenericCallback OnAddedToUi; /// /// Event that is called when this element is removed from a , that is, when this element's is set to . /// public GenericCallback OnRemovedFromUi; /// /// Event that is called when this element's method is called. /// This event is useful for unregistering global event handlers when this object should be destroyed. /// [Obsolete("OnDisposed will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")] 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; /// /// The of this element's , or the if this element has no parent. /// This value is the one that is passed to during . /// protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.System.Viewport; private readonly List children = new List(); private readonly Stopwatch stopwatch = new Stopwatch(); private bool sortedChildrenDirty; private IList sortedChildren; private UiSystem system; private Anchor anchor; private Vector2 size; private Vector2 autoSizeAddedAbsolute; private Vector2 offset; private RectangleF area; private bool isHidden; private int priority; private StyleProp style; private StyleProp childPadding; private bool canBeSelected = true; /// /// 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.GetTabNextElement = (backward, next) => next; this.GetGamepadNextElement = (dir, next) => next; this.SetAreaDirty(); this.SetSortedChildrenDirty(); } /// [Obsolete("Dispose will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")] ~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.AddedToUi(this.System, this.Root)); this.OnChildAdded?.Invoke(this, element); 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); if (this.Root?.SelectedElement == element) this.Root.SelectElement(null); // 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.RemovedFromUi()); this.OnChildRemoved?.Invoke(this, element); 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; this.Parent?.OnChildAreaDirty(this, false); } /// /// 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 || this.System == null) 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; this.stopwatch.Restart(); var parentArea = this.ParentArea; var parentCenterX = parentArea.X + parentArea.Width / 2; var parentCenterY = parentArea.Y + parentArea.Height / 2; var actualSize = this.CalcActualSize(parentArea); var recursion = 0; UpdateDisplayArea(actualSize); this.stopwatch.Stop(); this.System.Metrics.ForceAreaUpdateTime += this.stopwatch.Elapsed; this.System.Metrics.ForceAreaUpdates++; 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.IsAuto()) { 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 + Element.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; } this.SetAreaAndUpdateChildren(new RectangleF(pos, newSize)); 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) { if (lowest.Anchor.IsTopAligned()) { autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom; } else { autoSize.Y = lowest.UnscrolledArea.Height + this.ScaledChildPadding.Height; } foundChild = lowest; } else { autoSize.Y = 0; } } if (this.SetWidthBasedOnChildren) { var rightmost = this.GetRightmostChild(e => !e.IsHidden); if (rightmost != null) { if (rightmost.Anchor.IsLeftAligned()) { autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right; } else { autoSize.X = rightmost.UnscrolledArea.Width + this.ScaledChildPadding.Width; } foundChild = rightmost; } else { 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, Element.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?"); UpdateDisplayArea(autoSize); } } } } /// /// Sets this element's to the given and invokes the event. /// This method also updates all of this element's 's areas. /// Note that this method does not take into account any auto-sizing, anchoring or positioning, and so it should be used sparingly, if at all. /// /// public virtual void SetAreaAndUpdateChildren(RectangleF area) { this.area = area; this.System.InvokeOnElementAreaUpdated(this); foreach (var child in this.Children) child.ForceUpdateArea(); this.System.Metrics.ActualAreaUpdates++; } /// /// 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.ScaledAutoSizeAddedAbsolute.X, this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y + this.ScaledAutoSizeAddedAbsolute.Y); if (this.size.X < 0) ret.X = -this.size.X * ret.Y + this.ScaledAutoSizeAddedAbsolute.X; if (this.size.Y < 0) ret.Y = -this.size.Y * ret.X + this.ScaledAutoSizeAddedAbsolute.Y; 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; var lowestX = float.MinValue; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; var x = !child.Anchor.IsTopAligned() ? child.UnscrolledArea.Height : child.UnscrolledArea.Bottom; if (x >= lowestX) { lowest = child; lowestX = x; } } 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; var rightmostX = float.MinValue; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; var x = !child.Anchor.IsLeftAligned() ? child.UnscrolledArea.Width : child.UnscrolledArea.Right; if (x >= rightmostX) { rightmost = child; rightmostX = x; } } 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); // update all sorted children, not just relevant ones, because they might become relevant or irrelevant through updates foreach (var child in this.SortedChildren) { if (child.System != null) child.Update(time); } if (this.System != null) this.System.Metrics.Updates++; } /// /// Draws this element by calling internally. /// If or is set, a new SpriteBatch.Begin 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 [Obsolete("Use DrawTransformed that takes a SpriteBatchContext instead")] public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) { this.DrawTransformed(time, batch, alpha, new SpriteBatchContext(SpriteSortMode.Deferred, blendState, samplerState, depthStencilState, null, effect, matrix)); } /// /// Draws this element by calling internally. /// If or is set, a new SpriteBatch.Begin 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 sprite batch context to use for drawing public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { #pragma warning disable CS0618 var customDraw = this.BeginImpl != null || this.Transform != Matrix.Identity; var transformed = context; transformed.TransformMatrix = this.Transform * transformed.TransformMatrix; // TODO ending and beginning again when the matrix changes isn't ideal (https://github.com/MonoGame/MonoGame/issues/3156) if (customDraw) { // end the usual draw so that we can begin our own batch.End(); // begin our own draw call batch.Begin(transformed); } #pragma warning restore CS0618 // draw content in custom begin call #pragma warning disable CS0618 this.Draw(time, batch, alpha, transformed.BlendState, transformed.SamplerState, transformed.DepthStencilState, transformed.Effect, transformed.TransformMatrix); #pragma warning restore CS0618 if (this.System != null) this.System.Metrics.Draws++; if (customDraw) { // end our draw batch.End(); // begin the usual draw again for other elements batch.Begin(context); } } /// /// Draws this element and all of its children. Override this method to draw the content of custom elements. /// Note that, when this is called, SpriteBatch.Begin 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 [Obsolete("Use Draw that takes a SpriteBatchContext instead")] public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix) { this.Draw(time, batch, alpha, new SpriteBatchContext(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, SpriteBatch.Begin 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 sprite batch context to use for drawing public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) { 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) { #pragma warning disable CS0618 child.DrawTransformed(time, batch, alpha * child.DrawAlpha, context.BlendState, context.SamplerState, context.DepthStencilState, context.Effect, context.TransformMatrix); #pragma warning restore CS0618 } } } /// /// 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, SpriteBatch.Begin 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 [Obsolete("DrawEarly is deprecated. For custom implementations, see Panel.Draw for how to replace this method.")] 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. [Obsolete("Dispose will be removed in a future update. To unregister custom event handlers, use OnRemovedFromUi instead.")] 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 = this.SelectionIndicator.OrStyle(style.SelectionIndicator); this.ActionSound = this.ActionSound.OrStyle(style.ActionSound); this.SecondActionSound = this.SecondActionSound.OrStyle(style.ActionSound); this.System?.InvokeOnElementStyleInit(this); } /// /// A method that gets called by this element's or any of its grandchildren when their methods get called. /// Note that the element's area might already be dirty, which will not stop this method from being called. /// /// The child whose area is being set dirty. /// Whether the is a grandchild of this element, rather than a direct child. protected virtual void OnChildAreaDirty(Element child, bool grandchild) { if (!grandchild) { if (child.Anchor.IsAuto() || child.PreventParentSpill || this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren) this.SetAreaDirty(); } this.Parent?.OnChildAreaDirty(child, true); } /// /// 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); } /// /// Called when this element is added to a and, optionally, a given . /// This method is called in and . /// /// The ui system to add to. /// The root element to add to. protected internal virtual void AddedToUi(UiSystem system, RootElement root) { this.Root = root; this.System = system; this.OnAddedToUi?.Invoke(this); root?.InvokeOnElementAdded(this); } /// /// Called when this element is removed from a and . /// This method is called in and . /// protected internal virtual void RemovedFromUi() { var root = this.Root; this.Root = null; this.System = null; this.OnRemovedFromUi?.Invoke(this); root?.InvokeOnElementRemoved(this); } /// /// 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 [Obsolete("BeginDelegate is deprecated. You can create a custom element class and override Draw instead.")] public delegate void BeginDelegate(Element element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, Effect effect, Matrix matrix); } }