2019-08-09 18:26:28 +02:00
using System ;
using System.Collections.Generic ;
2021-03-24 01:39:41 +01:00
using System.Collections.ObjectModel ;
using System.Linq ;
2019-08-09 18:26:28 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2019-08-10 18:41:56 +02:00
using Microsoft.Xna.Framework.Input ;
2019-08-09 18:26:28 +02:00
using MLEM.Extensions ;
2019-08-09 22:04:26 +02:00
using MLEM.Input ;
2019-09-09 17:12:36 +02:00
using MLEM.Misc ;
2019-08-28 18:27:17 +02:00
using MLEM.Textures ;
2019-08-10 21:37:10 +02:00
using MLEM.Ui.Style ;
2021-07-05 16:36:48 +02:00
using SoundEffectInfo = MLEM . Sound . SoundEffectInfo ;
2019-08-09 18:26:28 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// This class represents a generic base class for ui elements of a <see cref="UiSystem"/>.
/// </summary>
2021-06-09 00:27:50 +02:00
public abstract class Element : GenericDataHolder , IDisposable {
2019-08-09 18:26:28 +02:00
2021-10-28 23:26:42 +02:00
/// <summary>
/// This field holds an epsilon value used in element <see cref="Size"/>, position and resulting <see cref="Area"/> calculations to mitigate floating point rounding inaccuracies.
/// If ui elements used are extremely small or extremely large, this value can be reduced or increased.
/// </summary>
public static float Epsilon = 0.01F ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The ui system that this element is currently a part of
/// </summary>
2020-02-06 01:59:33 +01:00
public UiSystem System {
get = > this . system ;
internal set {
this . system = value ;
2020-03-17 20:02:23 +01:00
this . Controls = value ? . Controls ;
2022-01-22 23:34:52 +01:00
this . Style = this . Style . OrStyle ( value ? . Style ) ;
2020-02-06 01:59:33 +01:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The controls that this element's <see cref="System"/> uses
/// </summary>
2020-03-17 20:04:10 +01:00
public UiControls Controls ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This element's parent element.
/// If this element has no parent (it is the <see cref="RootElement"/> of a ui system), this value is <c>null</c>.
/// </summary>
2020-02-06 01:59:33 +01:00
public Element Parent { get ; private set ; }
2020-05-22 17:02:24 +02:00
/// <summary>
/// This element's <see cref="RootElement"/>.
/// Note that this value is set even if this element has a <see cref="Parent"/>. To get the element that represents the root element, use <see cref="RootElement.Element"/>.
/// </summary>
2020-02-06 01:59:33 +01:00
public RootElement Root { get ; internal set ; }
2020-05-22 17:02:24 +02:00
/// <summary>
/// The scale that this ui element renders with
/// </summary>
2020-02-06 01:59:33 +01:00
public float Scale = > this . Root . ActualScale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="Anchor"/> that this element uses for positioning within its parent
/// </summary>
2019-08-09 18:26:28 +02:00
public Anchor Anchor {
get = > this . anchor ;
set {
2019-08-10 15:12:27 +02:00
if ( this . anchor = = value )
return ;
2019-08-09 18:26:28 +02:00
this . anchor = value ;
2019-08-12 14:44:42 +02:00
this . SetAreaDirty ( ) ;
2019-08-09 18:26:28 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-02 23:00:40 +02:00
/// 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).
/// </summary>
/// <example>
/// The following example combines both types of percentage-based sizing.
/// If this element is inside of a <see cref="Parent"/> whose width is 20, this element's width will be set to <c>0.5 * 20 = 10</c>, and its height will be set to <c>2.5 * 10 = 25</c>.
/// <code>
/// element.Size = new Vector2(0.5F, -2.5F);
/// </code>
/// </example>
2019-08-09 19:28:48 +02:00
public Vector2 Size {
2019-08-09 18:26:28 +02:00
get = > this . size ;
set {
2019-08-10 15:12:27 +02:00
if ( this . size = = value )
return ;
2019-08-09 18:26:28 +02:00
this . size = value ;
2019-08-12 14:44:42 +02:00
this . SetAreaDirty ( ) ;
2019-08-09 18:26:28 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="Size"/>, but with <see cref="Scale"/> applied.
/// </summary>
2019-08-11 21:24:09 +02:00
public Vector2 ScaledSize = > this . size * this . Scale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This element's offset from its default position, which is dictated by its <see cref="Anchor"/>.
/// Note that, depending on the side that the element is anchored to, this offset moves it in a different direction.
/// </summary>
2019-08-13 23:54:29 +02:00
public Vector2 PositionOffset {
2019-08-09 18:26:28 +02:00
get = > this . offset ;
set {
2019-08-10 15:12:27 +02:00
if ( this . offset = = value )
return ;
2019-08-09 18:26:28 +02:00
this . offset = value ;
2019-08-12 14:44:42 +02:00
this . SetAreaDirty ( ) ;
2019-08-09 18:26:28 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="PositionOffset"/>, but with <see cref="Scale"/> applied.
/// </summary>
2019-12-14 14:00:12 +01:00
public Vector2 ScaledOffset = > this . offset * this . Scale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="Padding"/>, but with <see cref="Scale"/> applied.
/// </summary>
2021-10-30 15:33:38 +02:00
public Padding ScaledPadding = > this . Padding . Value * this . Scale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="ChildPadding"/>, but with <see cref="Scale"/> applied.
/// </summary>
2021-10-30 15:33:38 +02:00
public Padding ScaledChildPadding = > this . ChildPadding . Value * this . Scale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This element's current <see cref="Area"/>, but with <see cref="ScaledChildPadding"/> applied.
/// </summary>
2020-02-06 01:59:33 +01:00
public RectangleF ChildPaddedArea = > this . UnscrolledArea . Shrink ( this . ScaledChildPadding ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// This element's area, without respecting its <see cref="ScrollOffset"/>.
/// This area is updated automatically to fit this element's sizing and positioning properties.
/// </summary>
2020-02-06 01:59:33 +01:00
public RectangleF UnscrolledArea {
get {
this . UpdateAreaIfDirty ( ) ;
return this . area ;
2019-08-10 21:37:10 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="UnscrolledArea"/> of this element, but with <see cref="ScaledScrollOffset"/> applied.
/// </summary>
2020-02-06 01:59:33 +01:00
public RectangleF Area = > this . UnscrolledArea . OffsetCopy ( this . ScaledScrollOffset ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The area that this element is displayed in, which is <see cref="Area"/> shrunk by this element's <see cref="ScaledPadding"/>.
/// This is the property that should be used for drawing this element, as well as mouse input handling and culling.
/// </summary>
2020-02-06 01:59:33 +01:00
public RectangleF DisplayArea = > this . Area . Shrink ( this . ScaledPadding ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The offset that this element has as a result of <see cref="Panel"/> scrolling.
/// </summary>
2020-02-06 01:59:33 +01:00
public Vector2 ScrollOffset ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="ScrollOffset"/>, but with <see cref="Scale"/> applied.
/// </summary>
2020-02-06 01:59:33 +01:00
public Vector2 ScaledScrollOffset = > this . ScrollOffset * this . Scale ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this property to <c>true</c> to cause this element to be hidden.
/// Hidden elements don't receive input events, aren't rendered and don't factor into auto-anchoring.
/// </summary>
2019-08-09 22:23:16 +02:00
public bool IsHidden {
get = > this . isHidden ;
set {
2019-08-10 15:12:27 +02:00
if ( this . isHidden = = value )
return ;
2019-08-09 22:23:16 +02:00
this . isHidden = value ;
2019-08-12 14:44:42 +02:00
this . SetAreaDirty ( ) ;
2019-08-09 22:23:16 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The priority of this element as part of its <see cref="Parent"/> element.
2021-12-11 17:26:55 +01:00
/// A higher priority means the element will be drawn first, but not anchored higher up if auto-anchoring is used.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-08-12 14:44:42 +02:00
public int Priority {
get = > this . priority ;
set {
2020-06-02 13:40:05 +02:00
if ( this . priority = = value )
return ;
2019-08-12 14:44:42 +02:00
this . priority = value ;
if ( this . Parent ! = null )
this . Parent . SetSortedChildrenDirty ( ) ;
}
}
2020-07-15 23:21:52 +02:00
/// <summary>
/// This element's transform matrix.
/// Can easily be scaled using <see cref="ScaleTransform"/>.
/// Note that, when this is non-null, a new <see cref="SpriteBatch.Begin"/> call is used for this element.
/// </summary>
2020-07-20 14:18:26 +02:00
public Matrix Transform = Matrix . Identity ;
2020-07-15 23:21:52 +02:00
/// <summary>
/// The call that this element should make to <see cref="SpriteBatch"/> to begin drawing.
/// Note that, when this is non-null, a new <see cref="SpriteBatch.Begin"/> call is used for this element.
/// </summary>
public BeginDelegate BeginImpl ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this field to false to disallow the element from being selected.
/// An unselectable element is skipped by automatic navigation and its <see cref="OnSelected"/> callback will never be called.
/// </summary>
2020-02-06 01:59:33 +01:00
public bool CanBeSelected = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this field to false to disallow the element from reacting to being moused over.
/// </summary>
2020-02-06 01:59:33 +01:00
public bool CanBeMoused = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this field to false to disallow this element's <see cref="OnPressed"/> and <see cref="OnSecondaryPressed"/> events to be called.
/// </summary>
2020-02-06 01:59:33 +01:00
public bool CanBePressed = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point.
/// </summary>
2020-02-06 01:59:33 +01:00
public bool CanAutoAnchorsAttach = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-03-24 01:25:39 +01:00
/// Set this field to true to cause this element's width to be automatically calculated based on the area that its <see cref="Children"/> take up.
2021-06-25 16:48:41 +02:00
/// To use this element's <see cref="Size"/>'s X coordinate as a minimum or maximum width rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
2021-03-24 01:25:39 +01:00
/// </summary>
public bool SetWidthBasedOnChildren ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// Set this field to true to cause this element's height to be automatically calculated based on the area that its <see cref="Children"/> take up.
2021-06-25 16:48:41 +02:00
/// To use this element's <see cref="Size"/>'s Y coordinate as a minimum or maximum height rather than ignoring it, set <see cref="TreatSizeAsMinimum"/> or <see cref="TreatSizeAsMaximum"/> to true.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-02-06 01:59:33 +01:00
public bool SetHeightBasedOnChildren ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-03-24 01:25:39 +01:00
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled, the resulting width or height will always be greather than or equal to this element's <see cref="Size"/>.
/// For example, if an element's <see cref="Size"/>'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 <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-03-24 01:25:39 +01:00
public bool TreatSizeAsMinimum ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-06-25 16:48:41 +02:00
/// If this field is set to true, and <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/>are enabled, the resulting width or height weill always be less than or equal to this element's <see cref="Size"/>.
/// Note that this value only has an effect if <see cref="SetWidthBasedOnChildren"/> or <see cref="SetHeightBasedOnChildren"/> are enabled.
/// </summary>
public bool TreatSizeAsMaximum ;
/// <summary>
2020-06-12 02:04:01 +02:00
/// Set this field to true to cause this element's final display area to never exceed that of its <see cref="Parent"/>.
/// 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.
/// </summary>
public bool PreventParentSpill ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// The transparency (alpha value) that this element is rendered with.
/// Note that, when <see cref="Draw"/> is called, this alpha value is multiplied with the <see cref="Parent"/>'s alpha value and passed down to this element's <see cref="Children"/>.
/// </summary>
2020-02-06 01:59:33 +01:00
public float DrawAlpha = 1 ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-04 20:52:21 +02:00
/// Stores whether this element is currently being moused over or touched.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-05-27 15:19:17 +02:00
public bool IsMouseOver { get ; protected set ; }
2020-05-22 17:02:24 +02:00
/// <summary>
/// Stores whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
/// </summary>
2020-05-27 15:19:17 +02:00
public bool IsSelected { get ; protected set ; }
2021-12-11 17:26:55 +01:00
/// <summary>
/// Returns whether this element's <see cref="SetAreaDirty"/> method has been recently called and its area has not been updated since then using <see cref="UpdateAreaIfDirty"/> or <see cref="ForceUpdateArea"/>.
/// </summary>
public bool AreaDirty { get ; private set ; }
2021-10-30 15:33:38 +02:00
2022-01-22 23:34:52 +01:00
/// <summary>
/// This Element's current <see cref="UiStyle"/>.
/// When this property is set, <see cref="InitStyle"/> is called.
/// </summary>
public StyleProp < UiStyle > Style {
get = > this . style ;
set {
this . style = value ;
if ( this . style . HasValue ( ) )
this . InitStyle ( this . style ) ;
}
}
2021-10-30 15:01:04 +02:00
/// <summary>
/// A style property that contains the selection indicator that is displayed on this element if it is the <see cref="RootElement.SelectedElement"/>
/// </summary>
public StyleProp < NinePatch > SelectionIndicator ;
/// <summary>
/// A style property that contains the sound effect that is played when this element's <see cref="OnPressed"/> is called
/// </summary>
public StyleProp < SoundEffectInfo > ActionSound ;
/// <summary>
/// A style property that contains the sound effect that is played when this element's <see cref="OnSecondaryPressed"/> is called
/// </summary>
public StyleProp < SoundEffectInfo > SecondActionSound ;
2021-10-30 15:33:38 +02:00
/// <summary>
/// The padding that this element has.
/// The padding is subtracted from the element's <see cref="Size"/>, and it is an area that the element does not extend into. This means that this element's resulting <see cref="DisplayArea"/> does not include this padding.
/// </summary>
public StyleProp < Padding > Padding ;
/// <summary>
/// The child padding that this element has.
/// The child padding moves any <see cref="Children"/> added to this element inwards by the given amount in each direction.
/// </summary>
2021-12-21 00:01:57 +01:00
public StyleProp < Padding > ChildPadding {
get = > this . childPadding ;
set {
this . childPadding = value ;
this . SetAreaDirty ( ) ;
}
}
2020-02-06 01:59:33 +01:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called after this element is drawn, but before its children are drawn
/// </summary>
2020-02-06 01:59:33 +01:00
public DrawCallback OnDrawn ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element is updated
/// </summary>
2020-02-06 01:59:33 +01:00
public TimeCallback OnUpdated ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element is pressed
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnPressed ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element is pressed using the secondary action
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnSecondaryPressed ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element's <see cref="IsSelected"/> is turned true
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnSelected ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element's <see cref="IsSelected"/> is turned false
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnDeselected ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element starts being moused over
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnMouseEnter ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element stops being moused over
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnMouseExit ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-04 20:52:21 +02:00
/// Event that is called when this element starts being touched
/// </summary>
public GenericCallback OnTouchEnter ;
/// <summary>
/// Event that is called when this element stops being touched
/// </summary>
public GenericCallback OnTouchExit ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// Event that is called when text input is made.
2021-04-23 00:17:46 +02:00
/// When an element uses this event, it should call <see cref="MlemPlatform.EnsureExists"/> on construction to ensure that the MLEM platform is initialized.
2020-05-22 17:02:24 +02:00
/// Note that this event is called for every element, even if it is not selected.
2021-04-23 00:17:46 +02:00
/// Also note that if the active <see cref="MlemPlatform"/> uses an on-screen keyboard, this event is never called.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-02-06 01:59:33 +01:00
public TextInputCallback OnTextInput ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when this element's <see cref="DisplayArea"/> is changed.
/// </summary>
2020-02-06 01:59:33 +01:00
public GenericCallback OnAreaUpdated ;
2020-05-22 17:02:24 +02:00
/// <summary>
2022-01-22 23:05:29 +01:00
/// Event that is called when this element's <see cref="InitStyle"/> method is called while setting the <see cref="Style"/>.
/// </summary>
public GenericCallback OnStyleInit ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// 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.
/// </summary>
2020-02-06 01:59:33 +01:00
public OtherElementCallback OnMousedElementChanged ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-04 20:52:21 +02:00
/// 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.
/// </summary>
public OtherElementCallback OnTouchedElementChanged ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// 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.
/// </summary>
2020-02-06 01:59:33 +01:00
public OtherElementCallback OnSelectedElementChanged ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// 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.
/// </summary>
2020-02-06 01:59:33 +01:00
public TabNextElementCallback GetTabNextElement ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// 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.
/// </summary>
2020-02-06 01:59:33 +01:00
public GamepadNextElementCallback GetGamepadNextElement ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when a child is added to this element using <see cref="AddChild{T}"/>
/// </summary>
2020-05-20 00:48:53 +02:00
public OtherElementCallback OnChildAdded ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>
/// </summary>
2020-05-20 00:48:53 +02:00
public OtherElementCallback OnChildRemoved ;
2021-06-09 00:27:50 +02:00
/// <summary>
/// Event that is called when this element's <see cref="Dispose"/> method is called, which also happens in <see cref="Finalize"/>.
/// This event is useful for unregistering global event handlers when this object should be destroyed.
/// </summary>
public GenericCallback OnDisposed ;
2020-02-06 01:59:33 +01:00
2020-05-22 17:02:24 +02:00
/// <summary>
2021-10-30 15:01:04 +02:00
/// A list of all of this element's direct children.
/// Use <see cref="AddChild{T}"/> or <see cref="RemoveChild"/> to manipulate this list while calling all of the necessary callbacks.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-10-30 15:01:04 +02:00
protected readonly IList < Element > Children ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-10-30 15:01:04 +02:00
/// A sorted version of <see cref="Children"/>. The children are sorted by their <see cref="Priority"/>.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-10-30 15:01:04 +02:00
protected IList < Element > SortedChildren {
get {
this . UpdateSortedChildrenIfDirty ( ) ;
return this . sortedChildren ;
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
2021-10-30 15:01:04 +02:00
/// The input handler that this element's <see cref="Controls"/> use
2020-05-22 17:02:24 +02:00
/// </summary>
2021-10-30 15:01:04 +02:00
protected InputHandler Input = > this . Controls . Input ;
2022-01-06 23:26:14 +01:00
/// <summary>
/// The <see cref="ChildPaddedArea"/> of this element's <see cref="Parent"/>, or the <see cref="UiSystem.Viewport"/> if this element has no parent.
/// This value is the one that is passed to <see cref="CalcActualSize"/> during <see cref="ForceUpdateArea"/>.
/// </summary>
protected RectangleF ParentArea = > this . Parent ? . ChildPaddedArea ? ? ( RectangleF ) this . system . Viewport ;
2021-10-30 15:01:04 +02:00
private readonly List < Element > children = new List < Element > ( ) ;
private bool sortedChildrenDirty ;
private IList < Element > sortedChildren ;
private UiSystem system ;
private Anchor anchor ;
private Vector2 size ;
private Vector2 offset ;
private RectangleF area ;
private bool isHidden ;
private int priority ;
2022-01-22 23:34:52 +01:00
private StyleProp < UiStyle > style ;
2021-12-21 00:01:57 +01:00
private StyleProp < Padding > childPadding ;
2019-08-09 18:26:28 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new element with the given anchor and size and sets up some default event reactions.
/// </summary>
/// <param name="anchor">This element's <see cref="Anchor"/></param>
/// <param name="size">This element's default <see cref="Size"/></param>
protected Element ( Anchor anchor , Vector2 size ) {
2019-08-09 18:26:28 +02:00
this . anchor = anchor ;
this . size = size ;
2019-08-09 22:04:26 +02:00
2021-03-24 01:39:41 +01:00
this . Children = new ReadOnlyCollection < Element > ( this . children ) ;
2019-08-11 18:02:21 +02:00
this . OnMouseEnter + = element = > this . IsMouseOver = true ;
this . OnMouseExit + = element = > this . IsMouseOver = false ;
2020-06-04 20:52:21 +02:00
this . OnTouchEnter + = element = > this . IsMouseOver = true ;
this . OnTouchExit + = element = > this . IsMouseOver = false ;
2019-08-10 18:41:56 +02:00
this . OnSelected + = element = > this . IsSelected = true ;
this . OnDeselected + = element = > this . IsSelected = false ;
2020-02-06 01:59:33 +01:00
this . GetTabNextElement + = ( backward , next ) = > next ;
this . GetGamepadNextElement + = ( dir , next ) = > next ;
2019-08-09 22:04:26 +02:00
2019-08-12 14:44:42 +02:00
this . SetAreaDirty ( ) ;
2021-03-24 01:39:41 +01:00
this . SetSortedChildrenDirty ( ) ;
2019-08-09 18:26:28 +02:00
}
2021-06-09 00:27:50 +02:00
/// <inheritdoc />
~ Element ( ) {
this . Dispose ( ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Adds a child to this element.
/// </summary>
/// <param name="element">The child element to add</param>
/// <param name="index">The index to add the child at, or -1 to add it to the end of the <see cref="Children"/> list</param>
/// <typeparam name="T">The type of child to add</typeparam>
/// <returns>This element, for chaining</returns>
2021-03-24 22:01:02 +01:00
public virtual T AddChild < T > ( T element , int index = - 1 ) where T : Element {
2021-03-24 01:39:41 +01:00
if ( index < 0 | | index > this . children . Count )
index = this . children . Count ;
this . children . Insert ( index , element ) ;
2019-08-23 22:23:10 +02:00
element . Parent = this ;
2019-09-02 18:41:05 +02:00
element . AndChildren ( e = > {
2019-08-28 18:58:05 +02:00
e . Root = this . Root ;
2020-02-06 01:51:41 +01:00
e . System = this . System ;
2021-06-09 00:27:50 +02:00
this . Root ? . InvokeOnElementAdded ( e ) ;
2020-05-20 00:48:53 +02:00
this . OnChildAdded ? . Invoke ( this , e ) ;
2019-08-28 18:58:05 +02:00
} ) ;
2019-08-12 14:44:42 +02:00
this . SetSortedChildrenDirty ( ) ;
2020-05-17 00:33:16 +02:00
element . SetAreaDirty ( ) ;
2019-08-09 22:23:16 +02:00
return element ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Removes the given child from this element.
/// </summary>
/// <param name="element">The child element to remove</param>
2021-03-24 22:01:02 +01:00
public virtual void RemoveChild ( Element element ) {
2021-03-24 01:39:41 +01:00
this . children . Remove ( element ) ;
2020-05-17 00:33:16 +02:00
// set area dirty here so that a dirty call is made
// upwards to us if the element is auto-positioned
element . SetAreaDirty ( ) ;
2019-08-23 22:23:10 +02:00
element . Parent = null ;
2019-09-02 18:41:05 +02:00
element . AndChildren ( e = > {
2019-08-28 18:58:05 +02:00
e . Root = null ;
2020-02-06 01:51:41 +01:00
e . System = null ;
2021-06-09 00:27:50 +02:00
this . Root ? . InvokeOnElementRemoved ( e ) ;
2020-05-20 00:48:53 +02:00
this . OnChildRemoved ? . Invoke ( this , e ) ;
2019-08-28 18:58:05 +02:00
} ) ;
2019-12-25 12:19:55 +01:00
this . SetSortedChildrenDirty ( ) ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Removes all children from this element that match the given condition.
/// </summary>
/// <param name="condition">The condition that determines if a child should be removed</param>
2021-03-24 22:01:02 +01:00
public virtual void RemoveChildren ( Func < Element , bool > condition = null ) {
2019-09-12 11:52:47 +02:00
for ( var i = this . Children . Count - 1 ; i > = 0 ; i - - ) {
2019-09-17 14:06:10 +02:00
var child = this . Children [ i ] ;
if ( condition = = null | | condition ( child ) ) {
this . RemoveChild ( child ) ;
}
2019-09-12 11:52:47 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Causes <see cref="SortedChildren"/> to be recalculated as soon as possible.
/// </summary>
2019-08-12 14:44:42 +02:00
public void SetSortedChildrenDirty ( ) {
this . sortedChildrenDirty = true ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Updates the <see cref="SortedChildren"/> list if <see cref="SetSortedChildrenDirty"/> is true.
/// </summary>
2019-08-12 14:44:42 +02:00
public void UpdateSortedChildrenIfDirty ( ) {
2019-11-18 22:36:55 +01:00
if ( this . sortedChildrenDirty )
this . ForceUpdateSortedChildren ( ) ;
}
2019-08-12 14:44:42 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Forces an update of the <see cref="SortedChildren"/> list.
/// </summary>
2019-11-18 22:36:55 +01:00
public virtual void ForceUpdateSortedChildren ( ) {
this . sortedChildrenDirty = false ;
2021-03-24 01:39:41 +01:00
this . sortedChildren = new ReadOnlyCollection < Element > ( this . Children . OrderBy ( e = > e . Priority ) . ToArray ( ) ) ;
2019-11-18 22:36:55 +01:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Causes this element's <see cref="Area"/> 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.
/// </summary>
2019-11-18 22:36:55 +01:00
public void SetAreaDirty ( ) {
2021-12-11 17:26:55 +01:00
this . AreaDirty = true ;
2022-01-09 01:12:16 +01:00
this . Parent ? . OnChildAreaDirty ( this , false ) ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
2021-12-11 17:26:55 +01:00
/// Updates this element's <see cref="Area"/> and all of its <see cref="Children"/> by calling <see cref="ForceUpdateArea"/> if <see cref="AreaDirty"/> is true.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-12-11 17:26:55 +01:00
/// <returns>Whether <see cref="AreaDirty"/> was true and <see cref="ForceUpdateArea"/> was called</returns>
2021-11-01 15:29:59 +01:00
public bool UpdateAreaIfDirty ( ) {
2021-12-11 17:26:55 +01:00
if ( this . AreaDirty ) {
2019-08-09 18:26:28 +02:00
this . ForceUpdateArea ( ) ;
2021-11-01 15:29:59 +01:00
return true ;
}
return false ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Forces this element's <see cref="Area"/> to be updated if it is not <see cref="IsHidden"/>.
/// This method also updates all of this element's <see cref="Children"/>'s areas.
/// </summary>
2019-08-09 19:28:48 +02:00
public virtual void ForceUpdateArea ( ) {
2021-12-11 17:26:55 +01:00
this . AreaDirty = false ;
2019-08-15 14:59:15 +02:00
if ( this . IsHidden )
return ;
2021-11-01 13:39:37 +01:00
// 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
2021-11-01 15:29:59 +01:00
if ( this . Parent ! = null & & this . Parent . UpdateAreaIfDirty ( ) )
2021-11-01 13:39:37 +01:00
return ;
2021-12-12 12:32:09 +01:00
this . System . Stopwatch . Restart ( ) ;
2019-08-09 18:26:28 +02:00
2022-01-06 23:26:14 +01:00
var parentArea = this . ParentArea ;
2019-08-09 18:26:28 +02:00
var parentCenterX = parentArea . X + parentArea . Width / 2 ;
var parentCenterY = parentArea . Y + parentArea . Height / 2 ;
2021-03-24 01:25:39 +01:00
var actualSize = this . CalcActualSize ( parentArea ) ;
2019-08-11 18:50:39 +02:00
2021-03-29 06:41:38 +02:00
var recursion = 0 ;
UpdateDisplayArea ( actualSize ) ;
2021-03-24 01:10:42 +01:00
2021-12-12 12:32:09 +01:00
this . System . Stopwatch . Stop ( ) ;
this . System . Metrics . ForceAreaUpdateTime + = this . System . Stopwatch . Elapsed ;
this . System . Metrics . ForceAreaUpdates + + ;
2021-03-24 01:25:39 +01:00
void UpdateDisplayArea ( Vector2 newSize ) {
2021-03-24 01:10:42 +01:00
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 :
2021-03-24 01:25:39 +01:00
pos . X = parentCenterX - newSize . X / 2 + this . ScaledOffset . X ;
2021-03-24 01:10:42 +01:00
pos . Y = parentArea . Y + this . ScaledOffset . Y ;
break ;
case Anchor . TopRight :
case Anchor . AutoRight :
2021-03-24 01:25:39 +01:00
pos . X = parentArea . Right - newSize . X - this . ScaledOffset . X ;
2021-03-24 01:10:42 +01:00
pos . Y = parentArea . Y + this . ScaledOffset . Y ;
break ;
case Anchor . CenterLeft :
pos . X = parentArea . X + this . ScaledOffset . X ;
2021-03-24 01:25:39 +01:00
pos . Y = parentCenterY - newSize . Y / 2 + this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
break ;
case Anchor . Center :
2021-03-24 01:25:39 +01:00
pos . X = parentCenterX - newSize . X / 2 + this . ScaledOffset . X ;
pos . Y = parentCenterY - newSize . Y / 2 + this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
break ;
case Anchor . CenterRight :
2021-03-24 01:25:39 +01:00
pos . X = parentArea . Right - newSize . X - this . ScaledOffset . X ;
pos . Y = parentCenterY - newSize . Y / 2 + this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
break ;
case Anchor . BottomLeft :
pos . X = parentArea . X + this . ScaledOffset . X ;
2021-03-24 01:25:39 +01:00
pos . Y = parentArea . Bottom - newSize . Y - this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
break ;
case Anchor . BottomCenter :
2021-03-24 01:25:39 +01:00
pos . X = parentCenterX - newSize . X / 2 + this . ScaledOffset . X ;
pos . Y = parentArea . Bottom - newSize . Y - this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
break ;
case Anchor . BottomRight :
2021-03-24 01:25:39 +01:00
pos . X = parentArea . Right - newSize . X - this . ScaledOffset . X ;
pos . Y = parentArea . Bottom - newSize . Y - this . ScaledOffset . Y ;
2021-03-24 01:10:42 +01:00
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 ) ;
2020-09-23 00:41:24 +02:00
} else {
2021-03-24 01:10:42 +01:00
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 ;
2021-10-28 23:26:42 +02:00
// with awkward ui scale values, floating point rounding can cause an element that would usually
// be positioned correctly to be pushed into the next line due to a very small deviation
if ( newX + newSize . X < = parentArea . Right + Epsilon ) {
2021-03-24 01:10:42 +01:00
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 ;
}
2020-09-23 00:41:24 +02:00
}
}
2021-03-24 01:10:42 +01:00
if ( this . PreventParentSpill ) {
if ( pos . X < parentArea . X )
pos . X = parentArea . X ;
if ( pos . Y < parentArea . Y )
pos . Y = parentArea . Y ;
2021-03-24 01:25:39 +01:00
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 ;
2021-03-24 01:10:42 +01:00
}
2021-12-11 17:39:49 +01:00
this . SetAreaAndUpdateChildren ( new RectangleF ( pos , newSize ) ) ;
2021-03-29 06:41:38 +02:00
2021-04-26 19:21:11 +02:00
if ( this . SetWidthBasedOnChildren | | this . SetHeightBasedOnChildren ) {
2021-03-29 06:41:38 +02:00
Element foundChild = null ;
2021-03-29 08:28:49 +02:00
var autoSize = this . UnscrolledArea . Size ;
2021-06-09 00:27:50 +02:00
2021-03-29 06:41:38 +02:00
if ( this . SetHeightBasedOnChildren ) {
var lowest = this . GetLowestChild ( e = > ! e . IsHidden ) ;
2021-03-29 08:28:49 +02:00
if ( lowest ! = null ) {
2021-04-26 19:06:54 +02:00
autoSize . Y = lowest . UnscrolledArea . Bottom - pos . Y + this . ScaledChildPadding . Bottom ;
foundChild = lowest ;
2021-04-26 18:55:18 +02:00
} else {
2021-09-09 16:53:12 +02:00
if ( this . Children . Any ( e = > ! e . IsHidden ) )
2021-09-09 17:02:29 +02:00
throw new InvalidOperationException ( $"{this} with root {this.Root.Name} sets its height based on children but it only has visible children anchored too low ({string.Join(" , ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})" ) ;
2021-04-26 18:55:18 +02:00
autoSize . Y = 0 ;
2021-03-29 06:41:38 +02:00
}
}
2021-06-09 00:27:50 +02:00
2021-03-29 06:41:38 +02:00
if ( this . SetWidthBasedOnChildren ) {
var rightmost = this . GetRightmostChild ( e = > ! e . IsHidden ) ;
2021-03-29 08:28:49 +02:00
if ( rightmost ! = null ) {
2021-04-26 19:06:54 +02:00
autoSize . X = rightmost . UnscrolledArea . Right - pos . X + this . ScaledChildPadding . Right ;
foundChild = rightmost ;
2021-04-26 18:55:18 +02:00
} else {
2021-09-09 16:53:12 +02:00
if ( this . Children . Any ( e = > ! e . IsHidden ) )
2021-09-09 17:02:29 +02:00
throw new InvalidOperationException ( $"{this} with root {this.Root.Name} sets its width based on children but it only has visible children anchored too far right ({string.Join(" , ", this.Children.Where(c => !c.IsHidden).Select(c => c.Anchor))})" ) ;
2021-04-26 18:55:18 +02:00
autoSize . X = 0 ;
2021-03-29 06:41:38 +02:00
}
}
2021-06-09 00:27:50 +02:00
2021-06-25 16:48:41 +02:00
if ( this . TreatSizeAsMinimum ) {
2021-03-29 06:41:38 +02:00
autoSize = Vector2 . Max ( autoSize , actualSize ) ;
2021-06-25 16:48:41 +02:00
} else if ( this . TreatSizeAsMaximum ) {
autoSize = Vector2 . Min ( autoSize , actualSize ) ;
}
2021-09-24 16:35:53 +02:00
// we want to leave some leeway to prevent float rounding causing an infinite loop
2021-10-28 23:26:42 +02:00
if ( ! autoSize . Equals ( this . UnscrolledArea . Size , Epsilon ) ) {
2021-03-29 06:41:38 +02:00
recursion + + ;
if ( recursion > = 16 ) {
2021-04-27 21:17:06 +02:00
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?" ) ;
2021-03-29 06:41:38 +02:00
} else {
UpdateDisplayArea ( autoSize ) ;
}
}
}
2019-08-11 18:50:39 +02:00
}
2019-08-09 18:26:28 +02:00
}
2021-12-11 17:26:55 +01:00
/// <summary>
/// Sets this element's <see cref="Area"/> to the given <see cref="RectangleF"/> and invokes the <see cref="UiSystem.OnElementAreaUpdated"/> event.
/// This method also updates all of this element's <see cref="Children"/>'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.
/// </summary>
/// <seealso cref="ForceUpdateArea"/>
2021-12-11 17:39:49 +01:00
public virtual void SetAreaAndUpdateChildren ( RectangleF area ) {
2021-12-11 17:26:55 +01:00
this . area = area ;
this . System . InvokeOnElementAreaUpdated ( this ) ;
foreach ( var child in this . Children )
child . ForceUpdateArea ( ) ;
2021-12-12 12:32:09 +01:00
this . System . Metrics . ActualAreaUpdates + + ;
2021-12-11 17:26:55 +01:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Calculates the actual size that this element should take up, based on the area that its parent encompasses.
2020-06-02 23:00:40 +02:00
/// By default, this is based on the information specified in <see cref="Size"/>'s documentation.
2020-05-22 17:02:24 +02:00
/// </summary>
/// <param name="parentArea">This parent's area, or the ui system's viewport if it has no parent</param>
/// <returns>The actual size of this element, taking <see cref="Scale"/> into account</returns>
2019-11-02 14:53:59 +01:00
protected virtual Vector2 CalcActualSize ( RectangleF parentArea ) {
2020-06-02 23:00:40 +02:00
var ret = new Vector2 (
2021-08-05 03:47:03 +02:00
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 )
2020-06-02 23:00:40 +02:00
ret . X = - this . size . X * ret . Y ;
2021-08-05 03:47:03 +02:00
if ( this . size . Y < 0 )
2020-06-02 23:00:40 +02:00
ret . Y = - this . size . Y * ret . X ;
return ret ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the area that should be used for determining where auto-anchoring children should attach.
/// </summary>
/// <returns>The area for auto anchors</returns>
2019-11-02 14:53:59 +01:00
protected virtual RectangleF GetAreaForAutoAnchors ( ) {
2019-08-24 22:27:47 +02:00
return this . UnscrolledArea ;
2019-08-18 17:49:52 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns this element's lowest child element (in terms of y position) that matches the given condition.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <returns>The lowest element, or null if no such element exists</returns>
2019-09-12 12:39:18 +02:00
public Element GetLowestChild ( Func < Element , bool > condition = null ) {
2019-08-24 14:34:08 +02:00
Element lowest = null ;
2019-11-05 21:33:45 +01:00
foreach ( var child in this . Children ) {
2019-09-12 12:39:18 +02:00
if ( condition ! = null & & ! condition ( child ) )
2019-08-24 14:34:08 +02:00
continue ;
if ( child . Anchor > Anchor . TopRight & & child . Anchor < Anchor . AutoLeft )
continue ;
2019-11-05 22:08:19 +01:00
if ( lowest = = null | | child . UnscrolledArea . Bottom > = lowest . UnscrolledArea . Bottom )
2019-08-24 14:34:08 +02:00
lowest = child ;
}
return lowest ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns this element's rightmost child (in terms of x position) that matches the given condition.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <returns>The rightmost element, or null if no such element exists</returns>
2019-11-05 21:44:51 +01:00
public Element GetRightmostChild ( Func < Element , bool > condition = null ) {
Element rightmost = null ;
foreach ( var child in this . Children ) {
if ( condition ! = null & & ! condition ( child ) )
continue ;
2019-11-06 15:05:46 +01:00
if ( child . Anchor < Anchor . AutoLeft & & child . Anchor ! = Anchor . TopLeft & & child . Anchor ! = Anchor . CenterLeft & & child . Anchor ! = Anchor . BottomLeft )
2019-11-05 21:44:51 +01:00
continue ;
2019-11-05 22:08:19 +01:00
if ( rightmost = = null | | child . UnscrolledArea . Right > = rightmost . UnscrolledArea . Right )
2019-11-05 21:44:51 +01:00
rightmost = child ;
}
return rightmost ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns this element's lowest sibling that is also older (lower in its parent's <see cref="Children"/> list) than this element that also matches the given condition.
/// The returned element's <see cref="Parent"/> will always be equal to this element's <see cref="Parent"/>.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <returns>The lowest older sibling of this element, or null if no such element exists</returns>
2019-09-12 12:39:18 +02:00
public Element GetLowestOlderSibling ( Func < Element , bool > condition = null ) {
2019-08-09 18:26:28 +02:00
if ( this . Parent = = null )
return null ;
2019-08-24 15:12:11 +02:00
Element lowest = null ;
2019-08-12 14:44:42 +02:00
foreach ( var child in this . Parent . Children ) {
2019-08-09 18:26:28 +02:00
if ( child = = this )
break ;
2019-09-12 12:39:18 +02:00
if ( condition ! = null & & ! condition ( child ) )
2019-08-24 15:12:11 +02:00
continue ;
2019-11-05 22:08:19 +01:00
if ( lowest = = null | | child . UnscrolledArea . Bottom > = lowest . UnscrolledArea . Bottom )
2019-08-24 15:12:11 +02:00
lowest = child ;
2019-08-09 18:26:28 +02:00
}
2019-08-24 15:12:11 +02:00
return lowest ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns this element's first older sibling that matches the given condition.
/// The returned element's <see cref="Parent"/> will always be equal to this element's <see cref="Parent"/>.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <returns>The older sibling, or null if no such element exists</returns>
2019-09-12 12:39:18 +02:00
public Element GetOlderSibling ( Func < Element , bool > condition = null ) {
2019-08-24 15:20:00 +02:00
if ( this . Parent = = null )
return null ;
Element older = null ;
foreach ( var child in this . Parent . Children ) {
if ( child = = this )
break ;
2019-09-12 12:39:18 +02:00
if ( condition ! = null & & ! condition ( child ) )
2019-08-24 15:20:00 +02:00
continue ;
older = child ;
}
return older ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns all of this element's siblings that match the given condition.
/// Siblings are elements that have the same <see cref="Parent"/> as this element.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <returns>This element's siblings</returns>
2019-09-12 12:39:18 +02:00
public IEnumerable < Element > GetSiblings ( Func < Element , bool > condition = null ) {
2019-08-13 21:23:20 +02:00
if ( this . Parent = = null )
yield break ;
foreach ( var child in this . Parent . Children ) {
2019-09-12 12:39:18 +02:00
if ( condition ! = null & & ! condition ( child ) )
2019-08-13 21:23:20 +02:00
continue ;
if ( child ! = this )
yield return child ;
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// 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.
/// </summary>
/// <param name="condition">The condition to match</param>
/// <param name="regardGrandchildren">If this value is true, children of children of this element are also searched</param>
/// <param name="ignoreFalseGrandchildren">If this value is true, children for which the condition does not match will not have their children searched</param>
/// <typeparam name="T">The type of children to search for</typeparam>
/// <returns>All children that match the condition</returns>
2019-09-12 12:39:18 +02:00
public IEnumerable < T > GetChildren < T > ( Func < T , bool > condition = null , bool regardGrandchildren = false , bool ignoreFalseGrandchildren = false ) where T : Element {
2019-08-18 18:32:34 +02:00
foreach ( var child in this . Children ) {
2019-09-12 12:39:18 +02:00
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 ) )
2019-08-18 18:32:34 +02:00
yield return cc ;
}
}
}
2020-05-22 17:02:24 +02:00
/// <inheritdoc cref="GetChildren{T}"/>
2019-11-05 21:33:45 +01:00
public IEnumerable < Element > GetChildren ( Func < Element , bool > condition = null , bool regardGrandchildren = false , bool ignoreFalseGrandchildren = false ) {
return this . GetChildren < Element > ( condition , regardGrandchildren , ignoreFalseGrandchildren ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the parent tree of this element.
/// The parent tree is this element's <see cref="Parent"/>, followed by its parent, and so on, up until the <see cref="RootElement"/>'s <see cref="RootElement.Element"/>.
/// </summary>
/// <returns>This element's parent tree</returns>
2019-08-21 17:00:22 +02:00
public IEnumerable < Element > GetParentTree ( ) {
if ( this . Parent = = null )
yield break ;
yield return this . Parent ;
foreach ( var parent in this . Parent . GetParentTree ( ) )
yield return parent ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns a subset of <see cref="Children"/> that are currently relevant in terms of drawing and input querying.
/// A <see cref="Panel"/> only returns elements that are currently in view here.
/// </summary>
/// <returns>This element's relevant children</returns>
2021-03-24 01:39:41 +01:00
protected virtual IList < Element > GetRelevantChildren ( ) {
2019-09-20 13:48:49 +02:00
return this . SortedChildren ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Updates this element and all of its <see cref="GetRelevantChildren"/>
/// </summary>
/// <param name="time">The game's time</param>
2019-08-09 19:28:48 +02:00
public virtual void Update ( GameTime time ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementUpdated ( this , time ) ;
2019-09-25 16:47:19 +02:00
2021-12-14 13:42:31 +01:00
foreach ( var child in this . GetRelevantChildren ( ) ) {
2020-02-06 01:51:41 +01:00
if ( child . System ! = null )
2019-12-05 14:59:53 +01:00
child . Update ( time ) ;
2021-12-14 13:42:31 +01:00
}
2021-12-12 12:32:09 +01:00
2021-12-14 13:42:31 +01:00
if ( this . System ! = null )
this . System . Metrics . Updates + + ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
2020-07-15 23:21:52 +02:00
/// Draws this element by calling <see cref="Draw"/> internally.
/// If <see cref="Transform"/> or <see cref="BeginImpl"/> is set, a new <see cref="SpriteBatch.Begin"/> call is also started.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="alpha">The alpha to draw this element and its children with</param>
/// <param name="blendState">The blend state that is used for drawing</param>
/// <param name="samplerState">The sampler state that is used for drawing</param>
2021-11-22 17:42:08 +01:00
/// <param name="effect">The effect that is used for drawing</param>
/// <param name="depthStencilState">The depth stencil state that is used for drawing</param>
2020-07-15 23:21:52 +02:00
/// <param name="matrix">The transformation matrix that is used for drawing</param>
2021-11-22 17:42:08 +01:00
public void DrawTransformed ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , DepthStencilState depthStencilState , Effect effect , Matrix matrix ) {
2020-07-20 14:18:26 +02:00
var customDraw = this . BeginImpl ! = null | | this . Transform ! = Matrix . Identity ;
var mat = this . Transform * matrix ;
2022-01-30 01:13:59 +01:00
// TODO ending and beginning again when the matrix changes isn't ideal (https://github.com/MonoGame/MonoGame/issues/3156)
2020-07-15 23:21:52 +02:00
if ( customDraw ) {
// end the usual draw so that we can begin our own
batch . End ( ) ;
// begin our own draw call
if ( this . BeginImpl ! = null ) {
2021-11-22 17:42:08 +01:00
this . BeginImpl ( this , time , batch , alpha , blendState , samplerState , depthStencilState , effect , mat ) ;
2020-07-15 23:21:52 +02:00
} else {
2021-11-22 17:42:08 +01:00
batch . Begin ( SpriteSortMode . Deferred , blendState , samplerState , depthStencilState , null , effect , mat ) ;
2020-07-15 23:21:52 +02:00
}
}
2021-12-14 13:42:31 +01:00
2020-07-15 23:21:52 +02:00
// draw content in custom begin call
2021-11-22 17:42:08 +01:00
this . Draw ( time , batch , alpha , blendState , samplerState , depthStencilState , effect , mat ) ;
2021-12-14 13:42:31 +01:00
if ( this . System ! = null )
this . System . Metrics . Draws + + ;
2020-07-15 23:21:52 +02:00
if ( customDraw ) {
// end our draw
batch . End ( ) ;
// begin the usual draw again for other elements
2021-11-22 17:42:08 +01:00
batch . Begin ( SpriteSortMode . Deferred , blendState , samplerState , depthStencilState , null , effect , matrix ) ;
2020-07-15 23:21:52 +02:00
}
}
/// <summary>
/// Draws this element and all of its children. Override this method to draw the content of custom elements.
/// Note that, when this is called, <see cref="SpriteBatch.Begin"/> has already been called with custom <see cref="Transform"/> etc. applied.
2020-05-22 17:02:24 +02:00
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="alpha">The alpha to draw this element and its children with</param>
/// <param name="blendState">The blend state that is used for drawing</param>
/// <param name="samplerState">The sampler state that is used for drawing</param>
2021-11-22 17:42:08 +01:00
/// <param name="effect">The effect that is used for drawing</param>
/// <param name="depthStencilState">The depth stencil state that is used for drawing</param>
2020-05-22 17:02:24 +02:00
/// <param name="matrix">The transformation matrix that is used for drawing</param>
2021-11-22 17:42:08 +01:00
public virtual void Draw ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , DepthStencilState depthStencilState , Effect effect , Matrix matrix ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementDrawn ( this , time , batch , alpha ) ;
2020-05-27 15:19:17 +02:00
if ( this . IsSelected )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnSelectedElementDrawn ( this , time , batch , alpha ) ;
2019-08-31 18:07:43 +02:00
2019-09-20 13:48:49 +02:00
foreach ( var child in this . GetRelevantChildren ( ) ) {
2019-08-09 19:39:51 +02:00
if ( ! child . IsHidden )
2021-11-22 17:42:08 +01:00
child . DrawTransformed ( time , batch , alpha * child . DrawAlpha , blendState , samplerState , depthStencilState , effect , matrix ) ;
2019-08-09 19:39:51 +02:00
}
2019-08-09 19:28:48 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Draws this element and all of its <see cref="GetRelevantChildren"/> early.
/// Drawing early involves drawing onto <see cref="RenderTarget2D"/> instances rather than onto the screen.
/// Note that, when this is called, <see cref="SpriteBatch.Begin"/> has not yet been called.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
/// <param name="alpha">The alpha to draw this element and its children with</param>
/// <param name="blendState">The blend state that is used for drawing</param>
/// <param name="samplerState">The sampler state that is used for drawing</param>
2021-11-22 17:42:08 +01:00
/// <param name="effect">The effect that is used for drawing</param>
/// <param name="depthStencilState">The depth stencil state that is used for drawing</param>
2020-05-22 17:02:24 +02:00
/// <param name="matrix">The transformation matrix that is used for drawing</param>
2022-01-30 01:13:59 +01:00
[Obsolete("DrawEarly has been deprecated. There is no replacement, and all drawing code should be placed in Draw.")]
2021-11-22 17:42:08 +01:00
public virtual void DrawEarly ( GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , DepthStencilState depthStencilState , Effect effect , Matrix matrix ) {
2019-09-20 13:48:49 +02:00
foreach ( var child in this . GetRelevantChildren ( ) ) {
2019-08-09 19:39:51 +02:00
if ( ! child . IsHidden )
2021-11-22 17:42:08 +01:00
child . DrawEarly ( time , batch , alpha * child . DrawAlpha , blendState , samplerState , depthStencilState , effect , matrix ) ;
2019-08-09 19:39:51 +02:00
}
2019-08-09 19:28:48 +02:00
}
2019-08-09 18:26:28 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the element under the given position, searching the current element and all of its <see cref="GetRelevantChildren"/>.
/// </summary>
/// <param name="position">The position to query</param>
/// <returns>The element under the position, or null if no such element exists</returns>
2019-11-02 14:53:59 +01:00
public virtual Element GetElementUnderPos ( Vector2 position ) {
2019-08-28 18:27:17 +02:00
if ( this . IsHidden )
2019-08-09 22:04:26 +02:00
return null ;
2021-11-22 15:13:08 +01:00
position = this . TransformInverse ( position ) ;
2021-03-24 01:39:41 +01:00
var relevant = this . GetRelevantChildren ( ) ;
for ( var i = relevant . Count - 1 ; i > = 0 ; i - - ) {
var element = relevant [ i ] . GetElementUnderPos ( position ) ;
2019-08-09 22:04:26 +02:00
if ( element ! = null )
return element ;
}
2019-09-26 19:36:15 +02:00
return this . CanBeMoused & & this . DisplayArea . Contains ( position ) ? this : null ;
2019-08-09 22:04:26 +02:00
}
2021-11-30 11:46:06 +01:00
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
2021-06-09 00:27:50 +02:00
public virtual void Dispose ( ) {
this . OnDisposed ? . Invoke ( this ) ;
GC . SuppressFinalize ( this ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Performs the specified action on this element and all of its <see cref="Children"/>
/// </summary>
/// <param name="action">The action to perform</param>
2019-09-02 18:41:05 +02:00
public void AndChildren ( Action < Element > action ) {
action ( this ) ;
foreach ( var child in this . Children )
child . AndChildren ( action ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Sorts this element's <see cref="Children"/> using the given comparison.
/// </summary>
/// <param name="comparison">The comparison to sort by</param>
2019-12-12 21:14:41 +01:00
public void ReorderChildren ( Comparison < Element > comparison ) {
2021-03-24 01:39:41 +01:00
this . children . Sort ( comparison ) ;
2019-12-12 21:14:41 +01:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Reverses this element's <see cref="Children"/> list in the given range.
/// </summary>
/// <param name="index">The index to start reversing at</param>
/// <param name="count">The amount of elements to reverse</param>
2019-12-19 12:52:31 +01:00
public void ReverseChildren ( int index = 0 , int? count = null ) {
2021-03-24 01:39:41 +01:00
this . children . Reverse ( index , count ? ? this . Children . Count ) ;
2019-12-19 12:52:31 +01:00
}
2020-07-15 23:21:52 +02:00
/// <summary>
/// Scales this element's <see cref="Transform"/> matrix based on the given scale and origin.
/// </summary>
/// <param name="scale">The scale to use</param>
/// <param name="origin">The origin to use for scaling, or null to use this element's center point</param>
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 ) ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Initializes this element's <see cref="StyleProp{T}"/> instances using the ui system's <see cref="UiStyle"/>.
/// </summary>
/// <param name="style">The new style</param>
2019-08-10 21:37:10 +02:00
protected virtual void InitStyle ( UiStyle style ) {
2021-12-21 11:54:32 +01:00
this . SelectionIndicator = this . SelectionIndicator . OrStyle ( style . SelectionIndicator ) ;
this . ActionSound = this . ActionSound . OrStyle ( style . ActionSound ) ;
this . SecondActionSound = this . SecondActionSound . OrStyle ( style . ActionSound ) ;
2022-01-22 23:34:52 +01:00
this . System ? . InvokeOnElementStyleInit ( this ) ;
2021-12-11 17:26:55 +01:00
}
/// <summary>
2022-01-09 01:12:16 +01:00
/// A method that gets called by this element's <see cref="Children"/> or any of its grandchildren when their <see cref="SetAreaDirty"/> methods get called.
2021-12-11 17:26:55 +01:00
/// Note that the element's area might already be dirty, which will not stop this method from being called.
/// </summary>
2022-01-09 01:12:16 +01:00
/// <param name="child">The child whose area is being set dirty.</param>
/// <param name="grandchild">Whether the <paramref name="child"/> is a grandchild of this element, rather than a direct child.</param>
protected virtual void OnChildAreaDirty ( Element child , bool grandchild ) {
if ( ! grandchild ) {
if ( child . Anchor > = Anchor . AutoLeft | | this . SetWidthBasedOnChildren | | this . SetHeightBasedOnChildren )
this . SetAreaDirty ( ) ;
}
this . Parent ? . OnChildAreaDirty ( child , true ) ;
2019-08-10 21:37:10 +02:00
}
2021-11-22 15:13:08 +01:00
/// <summary>
/// Transforms the given <paramref name="position"/> by the inverse of this element's <see cref="Transform"/> matrix.
/// </summary>
/// <param name="position">The position to transform</param>
/// <returns>The transformed position</returns>
protected Vector2 TransformInverse ( Vector2 position ) {
return this . Transform ! = Matrix . Identity ? Vector2 . Transform ( position , Matrix . Invert ( this . Transform ) ) : position ;
}
/// <summary>
/// Transforms the given <paramref name="position"/> by this element's <see cref="Root"/>'s <see cref="RootElement.InvTransform"/>, the inverses of all of the <see cref="Transform"/> matrices of this element's parent tree (<see cref="GetParentTree"/>), and the inverse of this element's <see cref="Transform"/> matrix.
/// Note that, when using <see cref="UiControls.GetElementUnderPos"/>, this operation is done recursively, which is more efficient.
/// </summary>
/// <param name="position">The position to transform</param>
/// <returns>The transformed position</returns>
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 ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate used for the <see cref="Element.OnTextInput"/> event.
/// </summary>
/// <param name="element">The current element</param>
/// <param name="key">The key that was pressed</param>
/// <param name="character">The character that was input</param>
2019-08-10 18:41:56 +02:00
public delegate void TextInputCallback ( Element element , Keys key , char character ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A generic element-specific delegate.
/// </summary>
/// <param name="element">The current element</param>
2019-08-10 18:41:56 +02:00
public delegate void GenericCallback ( Element element ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A generic element-specific delegate that includes a second element.
/// </summary>
/// <param name="thisElement">The current element</param>
/// <param name="otherElement">The other element</param>
2019-08-28 18:58:05 +02:00
public delegate void OtherElementCallback ( Element thisElement , Element otherElement ) ;
2019-08-11 18:02:21 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate used inside of <see cref="Element.Draw"/>
/// </summary>
/// <param name="element">The element that is being drawn</param>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch used for drawing</param>
/// <param name="alpha">The alpha this element is drawn with</param>
2019-09-04 17:19:31 +02:00
public delegate void DrawCallback ( Element element , GameTime time , SpriteBatch batch , float alpha ) ;
2019-08-31 18:07:43 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// A generic delegate used inside of <see cref="Element.Update"/>
/// </summary>
/// <param name="element">The current element</param>
/// <param name="time">The game's time</param>
2019-09-25 16:47:19 +02:00
public delegate void TimeCallback ( Element element , GameTime time ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate used by <see cref="Element.GetTabNextElement"/>.
/// </summary>
/// <param name="backward">If this value is true, <see cref="ModifierKey.Shift"/> is being held</param>
/// <param name="usualNext">The element that is considered to be the next element by default</param>
2019-09-11 10:49:51 +02:00
public delegate Element TabNextElementCallback ( bool backward , Element usualNext ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate used by <see cref="Element.GetGamepadNextElement"/>.
/// </summary>
/// <param name="dir">The direction of the gamepad button that was pressed</param>
/// <param name="usualNext">The element that is considered to be the next element by default</param>
2019-09-11 10:49:51 +02:00
public delegate Element GamepadNextElementCallback ( Direction2 dir , Element usualNext ) ;
2020-07-15 23:21:52 +02:00
/// <summary>
2020-07-16 15:55:55 +02:00
/// A delegate method used for <see cref="BeginImpl"/>
2020-07-15 23:21:52 +02:00
/// </summary>
/// <param name="element">The custom draw group</param>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch used for drawing</param>
/// <param name="alpha">This element's draw alpha</param>
/// <param name="blendState">The blend state used for drawing</param>
/// <param name="samplerState">The sampler state used for drawing</param>
2021-11-22 17:42:08 +01:00
/// <param name="effect">The effect used for drawing</param>
/// <param name="depthStencilState">The depth stencil state used for drawing</param>
2020-07-15 23:21:52 +02:00
/// <param name="matrix">The transform matrix used for drawing</param>
2021-11-22 17:42:08 +01:00
public delegate void BeginDelegate ( Element element , GameTime time , SpriteBatch batch , float alpha , BlendState blendState , SamplerState samplerState , DepthStencilState depthStencilState , Effect effect , Matrix matrix ) ;
2020-07-15 23:21:52 +02:00
2019-08-09 18:26:28 +02:00
}
}