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.Graphics; using MLEM.Input; using MLEM.Maths; 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 { /// /// 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.AndChildren(e => e.Style = e.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, which is used for drawing and mouse/touch interaction. All children and grandchildren of this element will have this transform applied to them, along with their own transforms. This can be useful for briefly changing the visuals of an element when using a . /// This matrix has no bearing on this element's permanent or , as it is only applied "on the fly" in and . /// When this is anything other than , a new SpriteBatch.Begin call is used for this element when drawing. /// This matrix can easily be scaled relative to its center or an arbitrary point using . /// public Matrix Transform = Matrix.Identity; /// /// 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 => this.canAutoAnchorsAttach; set { if (this.canAutoAnchorsAttach != value) { this.canAutoAnchorsAttach = value; this.SetAreaDirty(); } } } /// /// 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 => this.setWidthBasedOnChildren; set { if (this.setWidthBasedOnChildren != value) { this.setWidthBasedOnChildren = value; this.SetAreaDirty(); } } } /// /// 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 => this.setHeightBasedOnChildren; set { if (this.setHeightBasedOnChildren != value) { this.setHeightBasedOnChildren = value; this.SetAreaDirty(); } } } /// /// 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 => this.treatSizeAsMinimum; set { if (this.treatSizeAsMinimum != value) { this.treatSizeAsMinimum = value; this.SetAreaDirty(); } } } /// /// 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 => this.treatSizeAsMaximum; set { if (this.treatSizeAsMaximum != value) { this.treatSizeAsMaximum = value; this.SetAreaDirty(); } } } /// /// 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 => this.preventParentSpill; set { if (this.preventParentSpill != value) { this.preventParentSpill = value; this.SetAreaDirty(); } } } /// /// 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 . /// Note that, unlike , this property will be even if this element's is not the . /// public bool IsSelected => this.Root.SelectedElement == this; /// /// Returns whether this element is its 's . /// Note that can be used to query whether this element is its 's instead. /// public bool IsSelectedActive => this.Controls.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(); } } /// /// A that is played when the mouse enters this element, in . /// public StyleProp MouseEnterAnimation; /// /// A that is played when the mouse exits this element, in . /// public StyleProp MouseExitAnimation; /// /// 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; /// /// A list of all of this element's direct children. /// Use or to manipulate this list while calling all of the necessary callbacks. /// public readonly IList Children; /// /// A list of all of the instances that are currently playing. /// You can modify this collection through and . /// protected readonly List PlayingAnimations = new List(); /// /// 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; private bool canAutoAnchorsAttach = true; private bool setWidthBasedOnChildren; private bool setHeightBasedOnChildren; private bool treatSizeAsMinimum; private bool treatSizeAsMaximum; private bool preventParentSpill; /// /// 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.OnMouseEnter += e => { if (e.MouseEnterAnimation.HasValue()) e.PlayAnimation(e.MouseEnterAnimation); }; this.OnMouseExit += e => { if (e.MouseExitAnimation.HasValue()) e.PlayAnimation(e.MouseExitAnimation); }; this.SetAreaDirty(); this.SetSortedChildrenDirty(); } /// /// 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; if (this.System != null) 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; if (this.System != 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 recursion = 0; UpdateDisplayArea(); this.stopwatch.Stop(); this.System.Metrics.ForceAreaUpdateTime += this.stopwatch.Elapsed; this.System.Metrics.ForceAreaUpdates++; void UpdateDisplayArea(Vector2? overrideSize = null) { var parentArea = this.ParentArea; var parentCenterX = parentArea.X + parentArea.Width / 2; var parentCenterY = parentArea.Y + parentArea.Height / 2; var intendedSize = this.CalcActualSize(parentArea); var newSize = overrideSize ?? intendedSize; var pos = new Vector2(); switch (this.anchor) { case Anchor.TopLeft: case Anchor.AutoLeft: case Anchor.AutoInline: case Anchor.AutoInlineCenter: case Anchor.AutoInlineBottom: case Anchor.AutoInlineIgnoreOverflow: case Anchor.AutoInlineCenterIgnoreOverflow: case Anchor.AutoInlineBottomIgnoreOverflow: 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()) { if (this.Anchor.IsInline()) { var anchorEl = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); if (anchorEl != null) { var anchorElArea = anchorEl.GetAreaForAutoAnchors(); var newX = anchorElArea.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 (this.Anchor.IsIgnoreOverflow() || newX + newSize.X <= parentArea.Right + Element.Epsilon) { pos.X = newX; pos.Y = anchorElArea.Y + this.ScaledOffset.Y; if (this.Anchor == Anchor.AutoInlineCenter || this.Anchor == Anchor.AutoInlineCenterIgnoreOverflow) { pos.Y += (anchorElArea.Height - newSize.Y) / 2; } else if (this.Anchor == Anchor.AutoInlineBottom || this.Anchor == Anchor.AutoInlineBottomIgnoreOverflow) { pos.Y += anchorElArea.Height - newSize.Y; } } else { // inline anchors that overflow into the next line act like AutoLeft var newlineAnchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); if (newlineAnchorEl != null) pos.Y = newlineAnchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y; } } } else { // auto anchors keep their x coordinates from the switch above var anchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach); if (anchorEl != null) pos.Y = anchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y; } } 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, intendedSize); } else if (this.TreatSizeAsMaximum) { autoSize = Vector2.Min(autoSize, intendedSize); } // we want to leave some leeway to prevent float rounding causing an infinite loop if (!autoSize.Equals(this.UnscrolledArea.Size, Element.Epsilon)) { recursion++; this.System.Metrics.SummedRecursionDepth++; if (recursion > this.System.Metrics.MaxRecursionDepth) this.System.Metrics.MaxRecursionDepth = recursion; if (recursion >= 64) throw new ArithmeticException($"The area of {this} 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 /// Whether to evaluate based on the child's , rather than its . /// The lowest element, or null if no such element exists public Element GetLowestChild(Func condition = null, bool total = false) { Element lowest = null; var lowestX = float.MinValue; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea; var x = !child.Anchor.IsTopAligned() ? covered.Height : covered.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 /// Whether to evaluate based on the child's , rather than its . /// The rightmost element, or null if no such element exists public Element GetRightmostChild(Func condition = null, bool total = false) { Element rightmost = null; var rightmostX = float.MinValue; foreach (var child in this.Children) { if (condition != null && !condition(child)) continue; var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea; var x = !child.Anchor.IsLeftAligned() ? covered.Width : covered.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 /// Whether to evaluate based on the child's , rather than its . /// The lowest older sibling of this element, or null if no such element exists public Element GetLowestOlderSibling(Func condition = null, bool total = false) { 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 || (total ? child.GetTotalCoveredArea(true) : 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 the total covered area of this element, which is its (or ), unioned with all of the total covered areas of its . /// The returned area is only different from this element's (or ) if it has any that are outside of this element's area, or are bigger than this element. /// /// Whether to use elements' (instead of their ). /// This element's total covered area. public RectangleF GetTotalCoveredArea(bool unscrolled) { var ret = unscrolled ? this.UnscrolledArea : this.Area; foreach (var child in this.Children) { if (!child.IsHidden) ret = RectangleF.Union(ret, child.GetTotalCoveredArea(unscrolled)); } return ret; } /// /// 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); for (var i = this.PlayingAnimations.Count - 1; i >= 0; i--) { var anim = this.PlayingAnimations[i]; if (anim.Update(this, time)) { anim.OnFinished(this); this.PlayingAnimations.RemoveAt(i); } } // 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 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) { var customDraw = 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); } // draw content in custom begin call this.Draw(time, batch, alpha, transformed); 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 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, context); if (this.IsSelected) this.System.InvokeOnSelectedElementDrawn(this, time, batch, alpha, context); foreach (var child in this.GetRelevantChildren()) { if (!child.IsHidden) child.DrawTransformed(time, batch, alpha * child.DrawAlpha, context); } } /// /// 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; } /// /// Plays the given on this element, causing it to be added to the and updated in . /// If the given is already playing on this element, it will be restarted. /// /// The animation to play. public virtual void PlayAnimation(UiAnimation animation) { if (this.PlayingAnimations.Contains(animation)) { // if we're already playing this animation, just restart it animation.OnFinished(this); } else { this.PlayingAnimations.Add(animation); } } /// /// Stops the given on this element, causing it to be removed from the and to be invoked. /// /// The animation to stop. /// Whether the animation was present in this element's . public virtual bool StopAnimation(UiAnimation animation) { if (this.PlayingAnimations.Remove(animation)) { animation.OnFinished(this); return true; } return false; } /// public override string ToString() { var ret = this.GetType().Name; // elements will contain their path up to the root and their index in each parent // eg Paragraph 2 @ Panel 3 @ ... @ Group RootName if (this.Parent != null) { ret += $" {this.Parent.Children.IndexOf(this)} @ {this.Parent}"; } else if (this.Root?.Element == this) { ret += $" {this.Root.Name}"; } return ret; } /// /// 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. /// Please note the documentation for for in-depth information about how this matrix is used. /// /// 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.MouseEnterAnimation = this.MouseEnterAnimation.OrStyle(style.MouseEnterAnimation); this.MouseExitAnimation = this.MouseExitAnimation.OrStyle(style.MouseExitAnimation); this.System?.InvokeOnElementStyleInit(this); style.ApplyCustomStyle(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 for a parent whose is set, as well as . /// /// 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 for a parent whose is set, as well as . /// 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 /// The sprite batch context to use for drawing public delegate void DrawCallback(Element element, GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context); /// /// 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); } }