2019-08-09 18:26:28 +02:00
using System ;
using System.Collections.Generic ;
2021-12-12 12:32:09 +01:00
using System.Diagnostics ;
2019-08-28 18:27:17 +02:00
using System.Linq ;
2020-05-17 00:10:29 +02:00
using System.Text.RegularExpressions ;
2019-08-09 18:26:28 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
2020-05-17 00:10:29 +02:00
using MLEM.Formatting ;
using MLEM.Formatting.Codes ;
2019-08-09 22:04:26 +02:00
using MLEM.Input ;
2019-09-01 18:34:19 +02:00
using MLEM.Misc ;
2019-08-31 18:07:43 +02:00
using MLEM.Textures ;
2019-08-09 18:26:28 +02:00
using MLEM.Ui.Elements ;
2019-08-10 21:37:10 +02:00
using MLEM.Ui.Style ;
2019-08-09 18:26:28 +02:00
namespace MLEM.Ui {
2020-05-22 17:02:24 +02:00
/// <summary>
/// A ui system is the central location for the updating and rendering of all ui <see cref="Element"/>s.
/// Each element added to the root of the ui system is assigned a <see cref="RootElement"/> that has additional data like a transformation matrix.
2021-11-06 23:42:23 +01:00
/// For more information on how ui systems work, check out https://mlem.ellpeck.de/articles/ui.html.
2020-05-22 17:02:24 +02:00
/// </summary>
2019-12-05 17:52:25 +01:00
public class UiSystem : GameComponent {
2019-08-09 18:26:28 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// The viewport that this ui system is rendering inside of.
/// This is automatically updated during <see cref="GameWindow.ClientSizeChanged"/>
/// </summary>
2021-03-29 02:15:17 +02:00
public Rectangle Viewport ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this field to true to cause the ui system and all of its elements to automatically scale up or down with greater and lower resolution, respectively.
/// If this field is true, <see cref="AutoScaleReferenceSize"/> is used as the size that uses default <see cref="GlobalScale"/>
/// </summary>
2019-08-23 19:46:36 +02:00
public bool AutoScaleWithScreen ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// If <see cref="AutoScaleWithScreen"/> is true, this is used as the screen size that uses the default <see cref="GlobalScale"/>
/// </summary>
2019-08-23 19:46:36 +02:00
public Point AutoScaleReferenceSize ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The global rendering scale of this ui system and all of its child elements.
/// If <see cref="AutoScaleWithScreen"/> is true, this scale will be different based on the window size.
/// </summary>
2019-08-09 23:43:50 +02:00
public float GlobalScale {
2019-08-23 19:46:36 +02:00
get {
if ( ! this . AutoScaleWithScreen )
return this . globalScale ;
return Math . Min ( this . Viewport . Width / ( float ) this . AutoScaleReferenceSize . X , this . Viewport . Height / ( float ) this . AutoScaleReferenceSize . Y ) * this . globalScale ;
}
2019-08-09 23:43:50 +02:00
set {
this . globalScale = value ;
foreach ( var root in this . rootElements )
root . Element . ForceUpdateArea ( ) ;
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The style options that this ui system and all of its elements use.
/// To set the default, untextured style, use <see cref="UntexturedStyle"/>.
/// </summary>
2019-08-10 21:37:10 +02:00
public UiStyle Style {
get = > this . style ;
set {
this . style = value ;
foreach ( var root in this . rootElements ) {
2020-02-06 01:51:41 +01:00
root . Element . AndChildren ( e = > e . System = this ) ;
2020-05-20 21:22:02 +02:00
root . Element . ForceUpdateArea ( ) ;
2019-08-10 21:37:10 +02:00
}
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The transparency (alpha value) that this ui system and all of its elements draw at.
/// </summary>
2019-08-10 13:42:18 +02:00
public float DrawAlpha = 1 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The blend state that this ui system and all of its elements draw with
/// </summary>
2019-08-09 23:43:50 +02:00
public BlendState BlendState ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The sampler state that this ui system and all of its elements draw with.
/// The default is <see cref="Microsoft.Xna.Framework.Graphics.SamplerState.PointClamp"/>, as that is the one that works best with pixel graphics.
/// </summary>
2019-08-09 23:43:50 +02:00
public SamplerState SamplerState = SamplerState . PointClamp ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-11-22 17:42:08 +01:00
/// The depth stencil state that this ui system and all of its elements draw with.
/// The default is <see cref="Microsoft.Xna.Framework.Graphics.DepthStencilState.None"/>, which is also the default for <see cref="SpriteBatch.Begin"/>.
/// </summary>
public DepthStencilState DepthStencilState = DepthStencilState . None ;
/// <summary>
/// The effect that this ui system and all of its elements draw with.
/// The default is null, which means that no custom effect will be used.
/// </summary>
public Effect Effect ;
/// <summary>
2020-05-22 17:02:24 +02:00
/// The <see cref="TextFormatter"/> that this ui system's <see cref="Paragraph"/> elements format their text with.
/// To add new formatting codes to the ui system, add them to this formatter.
/// </summary>
2020-05-17 00:10:29 +02:00
public TextFormatter TextFormatter ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="UiControls"/> that this ui system is controlled by.
/// The ui controls are also the place to change bindings for controller and keyboard input.
/// </summary>
2019-08-28 18:27:17 +02:00
public UiControls Controls ;
2021-12-12 12:32:09 +01:00
/// <summary>
/// The update and rendering statistics to be used for runtime debugging and profiling.
/// The metrics are reset accordingly every frame: <see cref="UiMetrics.ResetUpdates"/> is called at the start of <see cref="Update"/>, and <see cref="UiMetrics.ResetDraws"/> is called at the start of <see cref="DrawEarly"/>, or at the start of <see cref="Draw"/> if <see cref="DrawEarly"/> was not called.
/// </summary>
public UiMetrics Metrics ;
2019-08-30 18:15:50 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked after an <see cref="Element"/> is drawn, but before its children are drawn.
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . DrawCallback OnElementDrawn ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked after the <see cref="RootElement.SelectedElement"/> for each root element is drawn, but before its children are drawn.
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . DrawCallback OnSelectedElementDrawn ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is updated
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . TimeCallback OnElementUpdated ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the primary action key
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementPressed ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the secondary action key
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementSecondaryPressed ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is newly selected using automatic navigation, or after it has been pressed with the mouse.
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementSelected ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is deselected during the selection of a new element.
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementDeselected ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when the mouse enters an <see cref="Element"/>
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementMouseEnter ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when the mouse exits an <see cref="Element"/>
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementMouseExit ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-04 20:52:21 +02:00
/// Event that is invoked when an <see cref="Element"/> starts being touched
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementTouchEnter ;
2020-06-04 20:52:21 +02:00
/// <summary>
/// Event that is invoked when an <see cref="Element"/> stops being touched
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementTouchExit ;
2020-06-04 20:52:21 +02:00
/// <summary>
2020-05-22 17:02:24 +02:00
/// Event that is invoked when an <see cref="Element"/>'s display area changes
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementAreaUpdated ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when the <see cref="Element"/> that the mouse is currently over changes
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnMousedElementChanged ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-04 20:52:21 +02:00
/// Event that is invoked when the <see cref="Element"/> that is being touched changes
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnTouchedElementChanged ;
2020-06-04 20:52:21 +02:00
/// <summary>
2020-05-22 17:02:24 +02:00
/// Event that is invoked when the selected <see cref="Element"/> changes, either through automatic navigation, or by pressing on an element with the mouse
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnSelectedElementChanged ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when a new <see cref="RootElement"/> is added to this ui system
/// </summary>
2021-06-09 00:27:50 +02:00
public event RootCallback OnRootAdded ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when a <see cref="RootElement"/> is removed from this ui system
/// </summary>
2021-06-09 00:27:50 +02:00
public event RootCallback OnRootRemoved ;
2019-08-31 18:07:43 +02:00
2021-12-12 12:32:09 +01:00
internal readonly Stopwatch Stopwatch = new Stopwatch ( ) ;
private readonly List < RootElement > rootElements = new List < RootElement > ( ) ;
private float globalScale = 1 ;
private bool drewEarly ;
private UiStyle style ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new ui system with the given settings.
/// </summary>
2021-02-18 18:36:29 +01:00
/// <param name="game">The game</param>
2020-05-22 17:02:24 +02:00
/// <param name="style">The style settings that this ui should have. Use <see cref="UntexturedStyle"/> for the default, untextured style.</param>
/// <param name="inputHandler">The input handler that this ui's <see cref="UiControls"/> should use. If none is supplied, a new input handler is created for this ui.</param>
2021-03-29 02:15:17 +02:00
/// <param name="automaticViewport">If this value is set to true, the ui system's <see cref="Viewport"/> will be set automatically based on the <see cref="GameWindow"/>'s size. Defaults to true.</param>
2021-03-29 02:26:44 +02:00
public UiSystem ( Game game , UiStyle style , InputHandler inputHandler = null , bool automaticViewport = true ) : base ( game ) {
2019-08-28 18:27:17 +02:00
this . Controls = new UiControls ( this , inputHandler ) ;
2019-08-10 21:37:10 +02:00
this . style = style ;
2021-06-09 00:27:50 +02:00
this . OnElementDrawn + = ( e , time , batch , alpha ) = > e . OnDrawn ? . Invoke ( e , time , batch , alpha ) ;
this . OnElementUpdated + = ( e , time ) = > e . OnUpdated ? . Invoke ( e , time ) ;
this . OnElementPressed + = e = > e . OnPressed ? . Invoke ( e ) ;
this . OnElementSecondaryPressed + = e = > e . OnSecondaryPressed ? . Invoke ( e ) ;
this . OnElementSelected + = e = > e . OnSelected ? . Invoke ( e ) ;
this . OnElementDeselected + = e = > e . OnDeselected ? . Invoke ( e ) ;
this . OnElementMouseEnter + = e = > e . OnMouseEnter ? . Invoke ( e ) ;
this . OnElementMouseExit + = e = > e . OnMouseExit ? . Invoke ( e ) ;
this . OnElementTouchEnter + = e = > e . OnTouchEnter ? . Invoke ( e ) ;
this . OnElementTouchExit + = e = > e . OnTouchExit ? . Invoke ( e ) ;
this . OnElementAreaUpdated + = e = > e . OnAreaUpdated ? . Invoke ( e ) ;
2021-06-09 00:37:44 +02:00
this . OnMousedElementChanged + = e = > this . ApplyToAll ( t = > t . OnMousedElementChanged ? . Invoke ( t , e ) ) ;
this . OnTouchedElementChanged + = e = > this . ApplyToAll ( t = > t . OnTouchedElementChanged ? . Invoke ( t , e ) ) ;
this . OnSelectedElementChanged + = e = > this . ApplyToAll ( t = > t . OnSelectedElementChanged ? . Invoke ( t , e ) ) ;
this . OnSelectedElementDrawn + = ( element , time , batch , alpha ) = > {
if ( this . Controls . IsAutoNavMode & & element . SelectionIndicator . HasValue ( ) )
2019-09-08 23:55:56 +02:00
batch . Draw ( element . SelectionIndicator , element . DisplayArea , Color . White * alpha , element . Scale / 2 ) ;
2019-08-31 18:07:43 +02:00
} ;
2020-01-14 22:39:40 +01:00
this . OnElementPressed + = e = > {
if ( e . OnPressed ! = null )
2020-06-22 13:59:33 +02:00
e . ActionSound . Value ? . Play ( ) ;
2020-01-14 22:39:40 +01:00
} ;
this . OnElementSecondaryPressed + = e = > {
if ( e . OnSecondaryPressed ! = null )
2020-06-22 13:59:33 +02:00
e . SecondActionSound . Value ? . Play ( ) ;
2020-01-14 22:39:40 +01:00
} ;
2021-06-09 00:37:44 +02:00
MlemPlatform . Current ? . AddTextInputListener ( game . Window , ( sender , key , character ) = > this . ApplyToAll ( e = > e . OnTextInput ? . Invoke ( e , key , character ) ) ) ;
if ( automaticViewport ) {
this . Viewport = new Rectangle ( Point . Zero , game . Window . ClientBounds . Size ) ;
this . AutoScaleReferenceSize = this . Viewport . Size ;
game . Window . ClientSizeChanged + = ( sender , args ) = > {
this . Viewport = new Rectangle ( Point . Zero , game . Window . ClientBounds . Size ) ;
foreach ( var root in this . rootElements )
root . Element . ForceUpdateArea ( ) ;
} ;
}
2020-05-17 00:10:29 +02:00
this . TextFormatter = new TextFormatter ( ) ;
2020-06-09 18:56:01 +02:00
this . TextFormatter . Codes . Add ( new Regex ( "<l(?: ([^>]+))?>" ) , ( f , m , r ) = > new LinkCode ( m , r , 1 / 16F , 0.85F ,
t = > this . Controls . MousedElement is Paragraph . Link l1 & & l1 . Token = = t | | this . Controls . TouchedElement is Paragraph . Link l2 & & l2 . Token = = t ) ) ;
2021-11-27 22:45:37 +01:00
this . TextFormatter . Codes . Add ( new Regex ( "<f ([^>]+)>" ) , ( _ , m , r ) = > new FontCode ( m , r ,
f = > this . Style . AdditionalFonts ! = null & & this . Style . AdditionalFonts . TryGetValue ( m . Groups [ 1 ] . Value , out var c ) ? c : f ) ) ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Update this ui system, querying the necessary events and updating each element's data.
/// </summary>
/// <param name="time">The game's time</param>
2019-12-05 17:52:25 +01:00
public override void Update ( GameTime time ) {
2021-12-12 12:32:09 +01:00
this . Metrics . ResetUpdates ( ) ;
this . Stopwatch . Restart ( ) ;
2019-08-10 18:41:56 +02:00
2021-12-12 12:32:09 +01:00
this . Controls . Update ( ) ;
for ( var i = this . rootElements . Count - 1 ; i > = 0 ; i - - )
2019-12-05 14:59:53 +01:00
this . rootElements [ i ] . Element . Update ( time ) ;
2021-12-12 12:32:09 +01:00
this . Stopwatch . Stop ( ) ;
this . Metrics . UpdateTime + = this . Stopwatch . Elapsed ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Draws any <see cref="Panel"/> and other elements that draw onto <see cref="RenderTarget2D"/> rather than directly onto the screen.
/// For drawing in this manner to work correctly, this method has to be called before your <see cref="GraphicsDevice"/> is cleared, and before everything else in your game is drawn.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
2019-08-15 14:59:15 +02:00
public void DrawEarly ( GameTime time , SpriteBatch batch ) {
2021-12-12 12:32:09 +01:00
this . Metrics . ResetDraws ( ) ;
this . Stopwatch . Restart ( ) ;
2019-08-12 19:44:16 +02:00
foreach ( var root in this . rootElements ) {
if ( ! root . Element . IsHidden )
2021-11-22 17:42:08 +01:00
root . Element . DrawEarly ( time , batch , this . DrawAlpha * root . Element . DrawAlpha , this . BlendState , this . SamplerState , this . DepthStencilState , this . Effect , root . Transform ) ;
2019-08-12 19:44:16 +02:00
}
2021-12-12 12:32:09 +01:00
this . Stopwatch . Stop ( ) ;
this . Metrics . DrawTime + = this . Stopwatch . Elapsed ;
this . drewEarly = true ;
2019-08-15 14:59:15 +02:00
}
2019-08-12 19:44:16 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Draws any <see cref="Element"/>s onto the screen.
/// Note that, when using <see cref="Panel"/>s with a scroll bar, <see cref="DrawEarly"/> needs to be called as well.
/// </summary>
/// <param name="time">The game's time</param>
/// <param name="batch">The sprite batch to use for drawing</param>
2019-08-15 14:59:15 +02:00
public void Draw ( GameTime time , SpriteBatch batch ) {
2021-12-12 12:32:09 +01:00
if ( ! this . drewEarly )
this . Metrics . ResetDraws ( ) ;
this . Stopwatch . Restart ( ) ;
2019-08-09 19:39:51 +02:00
foreach ( var root in this . rootElements ) {
2019-08-11 18:02:21 +02:00
if ( root . Element . IsHidden )
continue ;
2021-11-22 17:42:08 +01:00
batch . Begin ( SpriteSortMode . Deferred , this . BlendState , this . SamplerState , this . DepthStencilState , null , this . Effect , root . Transform ) ;
2019-09-20 13:22:05 +02:00
var alpha = this . DrawAlpha * root . Element . DrawAlpha ;
2021-11-22 17:42:08 +01:00
root . Element . DrawTransformed ( time , batch , alpha , this . BlendState , this . SamplerState , this . DepthStencilState , this . Effect , root . Transform ) ;
2019-08-11 18:02:21 +02:00
batch . End ( ) ;
2019-08-09 19:39:51 +02:00
}
2021-12-12 12:32:09 +01:00
this . Stopwatch . Stop ( ) ;
this . Metrics . DrawTime + = this . Stopwatch . Elapsed ;
this . drewEarly = false ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Adds a new root element to this ui system and returns the newly created <see cref="RootElement"/>.
/// Note that, when adding new elements that should be part of the same ui (like buttons on a panel), <see cref="Element.AddChild{T}"/> should be used.
/// </summary>
/// <param name="name">The name of the new root element</param>
/// <param name="element">The root element to add</param>
/// <returns>The newly created root element, or null if an element with the specified name already exists.</returns>
2019-08-23 22:23:10 +02:00
public RootElement Add ( string name , Element element ) {
2019-12-25 12:15:55 +01:00
if ( this . IndexOf ( name ) > = 0 )
return null ;
2019-08-23 22:23:10 +02:00
var root = new RootElement ( name , element , this ) ;
2019-12-25 12:15:55 +01:00
this . rootElements . Add ( root ) ;
2019-09-02 18:41:05 +02:00
root . Element . AndChildren ( e = > {
2019-08-28 18:58:05 +02:00
e . Root = root ;
2020-02-06 01:51:41 +01:00
e . System = this ;
2021-06-09 00:27:50 +02:00
root . InvokeOnElementAdded ( e ) ;
2019-11-18 02:20:09 +01:00
e . SetAreaDirty ( ) ;
2019-08-28 18:58:05 +02:00
} ) ;
2019-12-05 14:53:13 +01:00
this . OnRootAdded ? . Invoke ( root ) ;
2021-06-09 00:27:50 +02:00
root . InvokeOnAddedToUi ( this ) ;
2019-12-25 12:15:55 +01:00
this . SortRoots ( ) ;
return root ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Removes the <see cref="RootElement"/> with the specified name, or does nothing if there is no such element.
/// </summary>
/// <param name="name">The name of the root element to remove</param>
2019-08-09 18:26:28 +02:00
public void Remove ( string name ) {
2019-08-23 22:23:10 +02:00
var root = this . Get ( name ) ;
if ( root = = null )
2019-08-09 18:26:28 +02:00
return ;
2019-08-23 22:23:10 +02:00
this . rootElements . Remove ( root ) ;
2020-03-17 15:04:36 +01:00
this . Controls . SelectElement ( root , null ) ;
2019-09-02 18:41:05 +02:00
root . 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
root . InvokeOnElementRemoved ( e ) ;
2019-11-18 02:20:09 +01:00
e . SetAreaDirty ( ) ;
2019-08-28 18:58:05 +02:00
} ) ;
2019-12-05 14:53:13 +01:00
this . OnRootRemoved ? . Invoke ( root ) ;
2021-06-09 00:27:50 +02:00
root . InvokeOnRemovedFromUi ( this ) ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Finds the <see cref="RootElement"/> with the given name.
/// </summary>
/// <param name="name">The root element's name</param>
/// <returns>The root element with the given name, or null if no such element exists</returns>
2019-08-11 18:02:21 +02:00
public RootElement Get ( string name ) {
2019-08-09 18:26:28 +02:00
var index = this . IndexOf ( name ) ;
2019-08-11 18:02:21 +02:00
return index < 0 ? null : this . rootElements [ index ] ;
2019-08-09 18:26:28 +02:00
}
private int IndexOf ( string name ) {
return this . rootElements . FindIndex ( element = > element . Name = = name ) ;
}
2019-12-25 12:15:55 +01:00
internal void SortRoots ( ) {
// Normal list sorting isn't stable, but ordering is
var sorted = this . rootElements . OrderBy ( root = > root . Priority ) . ToArray ( ) ;
this . rootElements . Clear ( ) ;
this . rootElements . AddRange ( sorted ) ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns an enumerable of all of the <see cref="RootElement"/> instances that this ui system holds.
/// </summary>
/// <returns>All of this ui system's root elements</returns>
2019-08-28 18:27:17 +02:00
public IEnumerable < RootElement > GetRootElements ( ) {
for ( var i = this . rootElements . Count - 1 ; i > = 0 ; i - - )
yield return this . rootElements [ i ] ;
2019-08-28 18:58:05 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Applies the given action to all <see cref="Element"/> instances in this ui system recursively.
/// Note that, when this method is invoked, all root elements and all of their children receive the <see cref="Action"/>.
/// </summary>
/// <param name="action">The action to execute on each element</param>
2019-09-02 18:41:05 +02:00
public void ApplyToAll ( Action < Element > action ) {
2019-08-28 18:58:05 +02:00
foreach ( var root in this . rootElements )
2019-09-02 18:41:05 +02:00
root . Element . AndChildren ( action ) ;
2019-08-09 22:04:26 +02:00
}
2021-12-28 14:56:11 +01:00
internal void InvokeOnElementDrawn ( Element element , GameTime time , SpriteBatch batch , float alpha ) {
this . OnElementDrawn ? . Invoke ( element , time , batch , alpha ) ;
}
internal void InvokeOnSelectedElementDrawn ( Element element , GameTime time , SpriteBatch batch , float alpha ) {
this . OnSelectedElementDrawn ? . Invoke ( element , time , batch , alpha ) ;
}
internal void InvokeOnElementUpdated ( Element element , GameTime time ) {
this . OnElementUpdated ? . Invoke ( element , time ) ;
}
internal void InvokeOnElementAreaUpdated ( Element element ) {
this . OnElementAreaUpdated ? . Invoke ( element ) ;
}
internal void InvokeOnElementPressed ( Element element ) {
this . OnElementPressed ? . Invoke ( element ) ;
}
internal void InvokeOnElementSecondaryPressed ( Element element ) {
this . OnElementSecondaryPressed ? . Invoke ( element ) ;
}
internal void InvokeOnElementSelected ( Element element ) {
this . OnElementSelected ? . Invoke ( element ) ;
}
internal void InvokeOnElementDeselected ( Element element ) {
this . OnElementDeselected ? . Invoke ( element ) ;
}
internal void InvokeOnSelectedElementChanged ( Element element ) {
this . OnSelectedElementChanged ? . Invoke ( element ) ;
}
internal void InvokeOnElementMouseExit ( Element element ) {
this . OnElementMouseExit ? . Invoke ( element ) ;
}
internal void InvokeOnElementMouseEnter ( Element element ) {
this . OnElementMouseEnter ? . Invoke ( element ) ;
}
internal void InvokeOnMousedElementChanged ( Element element ) {
this . OnMousedElementChanged ? . Invoke ( element ) ;
}
internal void InvokeOnElementTouchExit ( Element element ) {
this . OnElementTouchExit ? . Invoke ( element ) ;
}
internal void InvokeOnElementTouchEnter ( Element element ) {
this . OnElementTouchEnter ? . Invoke ( element ) ;
}
internal void InvokeOnTouchedElementChanged ( Element element ) {
this . OnTouchedElementChanged ? . Invoke ( element ) ;
}
2021-06-09 00:27:50 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// A delegate used for callbacks that involve a <see cref="RootElement"/>
/// </summary>
/// <param name="root">The root element</param>
2019-12-05 14:53:13 +01:00
public delegate void RootCallback ( RootElement root ) ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// A root element is a wrapper around an <see cref="Element"/> that contains additional data.
/// Root elements are only used for the element in each element tree that doesn't have a <see cref="MLEM.Ui.Elements.Element.Parent"/>
/// To create a new root element, use <see cref="UiSystem.Add"/>
/// </summary>
2021-06-09 00:27:50 +02:00
public class RootElement : GenericDataHolder {
2019-08-09 18:26:28 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// The name of this root element
/// </summary>
2019-08-09 18:26:28 +02:00
public readonly string Name ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The element that this root element represents.
/// This is the only element in its family tree that does not have a <see cref="MLEM.Ui.Elements.Element.Parent"/>.
/// </summary>
2019-08-09 18:26:28 +02:00
public readonly Element Element ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The <see cref="UiSystem"/> that this root element is a part of.
/// </summary>
2020-02-06 01:51:41 +01:00
public readonly UiSystem System ;
2019-08-11 18:02:21 +02:00
private float scale = 1 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The scale of this root element.
/// Note that, to change the scale of every root element, you can use <see cref="UiSystem.GlobalScale"/>
/// </summary>
2019-08-11 18:02:21 +02:00
public float Scale {
get = > this . scale ;
set {
if ( this . scale = = value )
return ;
this . scale = value ;
this . Element . ForceUpdateArea ( ) ;
}
}
2019-12-25 12:15:55 +01:00
private int priority ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The priority of this root element.
/// A higher priority means the element will be updated first, as well as rendered on top.
/// </summary>
2019-12-25 12:15:55 +01:00
public int Priority {
get = > this . priority ;
set {
this . priority = value ;
2020-02-06 01:51:41 +01:00
this . System . SortRoots ( ) ;
2019-12-25 12:15:55 +01:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// The actual scale of this root element.
/// This is a combination of this root element's <see cref="Scale"/> as well as the ui system's <see cref="UiSystem.GlobalScale"/>.
/// </summary>
2020-02-06 01:51:41 +01:00
public float ActualScale = > this . System . GlobalScale * this . Scale ;
2019-08-09 18:26:28 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// The transformation that this root element (and all of its children) has.
/// This transform is applied both to input, as well as to rendering.
/// </summary>
2019-09-02 19:55:26 +02:00
public Matrix Transform = Matrix . Identity ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// An inversion of <see cref="Transform"/>
/// </summary>
2019-09-02 19:55:26 +02:00
public Matrix InvTransform = > Matrix . Invert ( this . Transform ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// The child element of this root element that is currently selected.
/// If there is no selected element in this root, this value will be <c>null</c>.
/// </summary>
2020-03-17 15:04:36 +01:00
public Element SelectedElement = > this . System . Controls . GetSelectedElement ( this ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Determines whether this root element contains any children that <see cref="Elements.Element.CanBeSelected"/>.
/// This value is automatically calculated.
/// </summary>
2019-12-08 21:49:15 +01:00
public bool CanSelectContent { get ; private set ; }
2020-05-22 17:02:24 +02:00
/// <summary>
/// Event that is invoked when a <see cref="Element"/> is added to this root element or any of its children.
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementAdded ;
2020-05-22 17:02:24 +02:00
/// <summary>
2021-06-08 21:36:42 +02:00
/// Event that is invoked when a <see cref="Element"/> is removed rom this root element of any of its children.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-06-09 00:27:50 +02:00
public event Element . GenericCallback OnElementRemoved ;
2021-06-08 21:36:42 +02:00
/// <summary>
/// Event that is invoked when this <see cref="RootElement"/> gets added to a <see cref="UiSystem"/> in <see cref="UiSystem.Add"/>
/// </summary>
2021-06-09 00:27:50 +02:00
public event Action < UiSystem > OnAddedToUi ;
2021-06-08 21:36:42 +02:00
/// <summary>
/// Event that is invoked when this <see cref="RootElement"/> gets removed from a <see cref="UiSystem"/> in <see cref="UiSystem.Remove"/>
/// </summary>
2021-06-09 00:27:50 +02:00
public event Action < UiSystem > OnRemovedFromUi ;
2019-09-09 17:12:36 +02:00
2020-05-22 17:02:24 +02:00
internal RootElement ( string name , Element element , UiSystem system ) {
2019-08-09 18:26:28 +02:00
this . Name = name ;
this . Element = element ;
2020-02-06 01:51:41 +01:00
this . System = system ;
2019-12-08 21:49:15 +01:00
this . OnElementAdded + = e = > {
if ( e . CanBeSelected )
this . CanSelectContent = true ;
} ;
this . OnElementRemoved + = e = > {
2021-07-18 22:18:46 +02:00
if ( e . CanBeSelected ) {
// check if removing this element removed all other selectable elements
foreach ( var c in this . Element . GetChildren ( regardGrandchildren : true ) ) {
if ( c . CanBeSelected )
return ;
}
2019-12-08 21:49:15 +01:00
this . CanSelectContent = false ;
2021-07-18 22:18:46 +02:00
}
2019-12-08 21:49:15 +01:00
} ;
2019-08-09 18:26:28 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Selects the given element that is a child of this root element.
/// Optionally, automatic navigation can be forced on, causing the <see cref="UiStyle.SelectionIndicator"/> to be drawn around the element.
/// </summary>
/// <param name="element">The element to select, or null to deselect the selected element.</param>
/// <param name="autoNav">Whether automatic navigation should be forced on</param>
2020-03-16 15:33:25 +01:00
public void SelectElement ( Element element , bool? autoNav = null ) {
2020-03-17 15:04:36 +01:00
this . System . Controls . SelectElement ( this , element , autoNav ) ;
2019-09-09 17:12:36 +02:00
}
2020-06-02 16:15:41 +02:00
/// <summary>
/// Scales this root 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 root's element's center point</param>
public void ScaleOrigin ( float scale , Vector2 ? origin = null ) {
this . Transform = Matrix . CreateScale ( scale , scale , 1 ) * Matrix . CreateTranslation ( new Vector3 ( ( 1 - scale ) * ( origin ? ? this . Element . DisplayArea . Center ) , 0 ) ) ;
}
2021-12-28 14:56:11 +01:00
internal void InvokeOnElementAdded ( Element element ) {
this . OnElementAdded ? . Invoke ( element ) ;
}
internal void InvokeOnElementRemoved ( Element element ) {
this . OnElementRemoved ? . Invoke ( element ) ;
}
internal void InvokeOnAddedToUi ( UiSystem system ) {
this . OnAddedToUi ? . Invoke ( system ) ;
}
internal void InvokeOnRemovedFromUi ( UiSystem system ) {
this . OnRemovedFromUi ? . Invoke ( system ) ;
}
2021-06-09 00:27:50 +02:00
2019-08-09 18:26:28 +02:00
}
}