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; namespace MLEM.Ui.Elements { /// /// This class represents a generic base class for ui elements of a . /// public abstract class Element : GenericDataHolder { /// /// 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; private readonly List children = new List(); /// /// A sorted version of . The children are sorted by their . /// protected IList SortedChildren { get { this.UpdateSortedChildrenIfDirty(); return this.sortedChildren; } } private bool sortedChildrenDirty; private IList sortedChildren; private UiSystem system; /// /// 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; if (this.system != null) this.InitStyle(this.system.Style); } } /// /// The controls that this element's uses /// public UiControls Controls; /// /// The input handler that this element's use /// protected InputHandler Input => this.Controls.Input; /// /// 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; private Anchor anchor; /// /// 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(); } } private Vector2 size; /// /// 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; private Vector2 offset; /// /// 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 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 Padding Padding; /// /// The , but with applied. /// public Padding ScaledPadding => this.Padding * this.Scale; private Padding childPadding; /// /// 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 Padding ChildPadding { get => this.childPadding; set { if (this.childPadding == value) return; this.childPadding = value; this.SetAreaDirty(); } } /// /// The , but with applied. /// public Padding ScaledChildPadding => this.childPadding * this.Scale; /// /// This element's current , but with applied. /// public RectangleF ChildPaddedArea => this.UnscrolledArea.Shrink(this.ScaledChildPadding); private RectangleF area; /// /// 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; } } private bool areaDirty; /// /// 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; private bool isHidden; /// /// 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(); } } private int priority; /// /// 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 width rather than ignoring it, set 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 height rather than ignoring it, set 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; /// /// 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 bool PreventParentSpill; /// /// 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; } /// /// 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 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; /// /// 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; /// /// 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(); } /// /// 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?.OnElementAdded(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?.OnElementRemoved(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 && (this.Anchor >= Anchor.AutoLeft || this.Parent.SetWidthBasedOnChildren || this.Parent.SetHeightBasedOnChildren)) this.Parent.SetAreaDirty(); } /// /// Updates this element's list if is true. /// public void UpdateAreaIfDirty() { if (this.areaDirty) this.ForceUpdateArea(); } /// /// 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; 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; if (newX + newSize.X <= parentArea.Right) { 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.area = new RectangleF(pos, newSize); this.System.OnElementAreaUpdated?.Invoke(this); foreach (var child in this.Children) child.ForceUpdateArea(); if (this.Children.Count > 0) { Element foundChild = null; var autoSize = this.UnscrolledArea.Size; if (this.SetHeightBasedOnChildren) { var lowest = this.GetLowestChild(e => !e.IsHidden); if (lowest != null) { var newHeight = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom; if (!newHeight.Equals(autoSize.Y, 0.01F)) { autoSize.Y = newHeight; foundChild = lowest; } } } if (this.SetWidthBasedOnChildren) { var rightmost = this.GetRightmostChild(e => !e.IsHidden); if (rightmost != null) { var newWidth = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right; if (!newWidth.Equals(autoSize.X, 0.01F)) { autoSize.X = newWidth; foundChild = rightmost; } } } if (this.TreatSizeAsMinimum) autoSize = Vector2.Max(autoSize, actualSize); if (foundChild != null) { 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.OnElementUpdated?.Invoke(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 transformation matrix that is used for drawing public void DrawTransformed(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, 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, mat); } else { batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, null, null, null, mat); } } // draw content in custom begin call this.Draw(time, batch, alpha, blendState, samplerState, mat); if (customDraw) { // end our draw batch.End(); // begin the usual draw again for other elements batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, null, null, null, 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 transformation matrix that is used for drawing public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) { this.System.OnElementDrawn?.Invoke(this, time, batch, alpha); if (this.IsSelected) this.System.OnSelectedElementDrawn?.Invoke(this, time, batch, alpha); foreach (var child in this.GetRelevantChildren()) { if (!child.IsHidden) child.DrawTransformed(time, batch, alpha * child.DrawAlpha, blendState, samplerState, 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 transformation matrix that is used for drawing public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) { foreach (var child in this.GetRelevantChildren()) { if (!child.IsHidden) child.DrawEarly(time, batch, alpha * child.DrawAlpha, blendState, samplerState, 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; if (this.Transform != Matrix.Identity) position = Vector2.Transform(position, Matrix.Invert(this.Transform)); 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 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); } /// /// 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 transform matrix used for drawing public delegate void BeginDelegate(Element element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix); } }