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.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.
/// 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 => 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;
///
/// 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.
///
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();
}
///
[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 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 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, context);
if (this.IsSelected)
this.System.InvokeOnSelectedElementDrawn(this, time, batch, alpha, context);
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;
}
///
/// 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;
}
/// 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);
}
///
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.
///
/// 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 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
/// 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);
///
/// 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);
}
}