2021-06-21 00:49:09 +02:00
using System ;
2022-05-18 15:54:29 +02:00
using System.Collections.Generic ;
2021-06-21 00:49:09 +02:00
using System.Linq ;
2019-08-24 12:40:20 +02:00
using Microsoft.Xna.Framework ;
2021-06-21 00:49:09 +02:00
using MLEM.Input ;
2021-10-30 15:33:38 +02:00
using MLEM.Misc ;
2019-08-27 21:44:02 +02:00
using MLEM.Textures ;
2019-08-24 12:40:20 +02:00
namespace MLEM.Ui.Elements {
2020-05-22 17:02:24 +02:00
/// <summary>
/// This class contains a set of helper methods that aid in creating special kinds of compound <see cref="Element"/> types for use inside of a <see cref="UiSystem"/>.
/// </summary>
2019-08-24 12:40:20 +02:00
public static class ElementHelper {
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a button with an image on the left side of its text.
/// </summary>
/// <param name="anchor">The button's anchor</param>
/// <param name="size">The button's size</param>
/// <param name="texture">The texture of the image to render on the button</param>
/// <param name="text">The text to display on the button</param>
/// <param name="tooltipText">The text of the button's tooltip</param>
/// <param name="imagePadding">The <see cref="Element.Padding"/> of the button's image</param>
/// <returns>An image button</returns>
2021-10-29 23:33:15 +02:00
public static Button ImageButton ( Anchor anchor , Vector2 size , TextureRegion texture , string text = null , string tooltipText = null , float imagePadding = 2 ) {
var button = new Button ( anchor , size , text , tooltipText ) ;
2021-10-30 15:33:38 +02:00
var image = new Image ( Anchor . CenterLeft , Vector2 . One , texture ) ;
2021-12-21 11:54:32 +01:00
image . Padding = image . Padding . OrStyle ( new Padding ( imagePadding ) , 1 ) ;
2019-08-27 21:44:02 +02:00
button . OnAreaUpdated + = e = > image . Size = new Vector2 ( e . Area . Height , e . Area . Height ) / e . Scale ;
button . AddChild ( image , 0 ) ;
return button ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a panel that contains a paragraph of text and a button to close the panel.
/// The panel is part of a group, which causes elements in the background (behind and around the panel) to not be clickable, leaving only the "close" button.
/// </summary>
/// <param name="system">The ui system to add the panel to, optional.</param>
/// <param name="anchor">The anchor of the panel</param>
/// <param name="width">The width of the panel</param>
/// <param name="text">The text to display on the panel</param>
/// <param name="buttonHeight">The height of the "close" button</param>
/// <param name="okText">The text on the "close" button</param>
/// <returns>An info box panel</returns>
2019-08-24 15:00:08 +02:00
public static Panel ShowInfoBox ( UiSystem system , Anchor anchor , float width , string text , float buttonHeight = 10 , string okText = "Okay" ) {
2019-12-25 19:16:07 +01:00
var group = new Group ( Anchor . TopLeft , Vector2 . One , false ) ;
var box = group . AddChild ( new Panel ( anchor , new Vector2 ( width , 1 ) , Vector2 . Zero , true ) ) ;
2019-08-24 15:00:08 +02:00
box . AddChild ( new Paragraph ( Anchor . AutoLeft , 1 , text ) ) ;
2019-09-09 17:12:36 +02:00
var button = box . AddChild ( new Button ( Anchor . AutoCenter , new Vector2 ( 0.5F , buttonHeight ) , okText ) {
2019-08-25 21:49:27 +02:00
OnPressed = element = > system . Remove ( "InfoBox" ) ,
2019-08-24 15:00:08 +02:00
PositionOffset = new Vector2 ( 0 , 1 )
} ) ;
2019-12-25 19:16:07 +01:00
var root = system . Add ( "InfoBox" , group ) ;
2019-09-09 17:12:36 +02:00
root . SelectElement ( button ) ;
2019-08-24 15:00:08 +02:00
return box ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates an array of groups with a fixed width that can be used to create a column structure
/// </summary>
/// <param name="parent">The element the groups should be added to, optional.</param>
/// <param name="totalSize">The total width of all of the groups combined</param>
/// <param name="amount">The amount of groups to split the total size into</param>
/// <param name="setHeightBasedOnChildren">Whether the groups should set their heights based on their children's heights</param>
/// <returns>An array of columns</returns>
2019-08-24 15:12:11 +02:00
public static Group [ ] MakeColumns ( Element parent , Vector2 totalSize , int amount , bool setHeightBasedOnChildren = true ) {
2019-08-24 14:34:08 +02:00
var cols = new Group [ amount ] ;
for ( var i = 0 ; i < amount ; i + + ) {
2019-09-11 15:03:10 +02:00
var anchor = i = = amount - 1 ? Anchor . AutoInlineIgnoreOverflow : Anchor . AutoInline ;
cols [ i ] = new Group ( anchor , new Vector2 ( totalSize . X / amount , totalSize . Y ) , setHeightBasedOnChildren ) ;
2022-10-17 10:57:41 +02:00
parent ? . AddChild ( cols [ i ] ) ;
2019-08-24 14:34:08 +02:00
}
return cols ;
}
2022-10-17 10:57:41 +02:00
/// <summary>
/// Creates an array of groups with a fixed width and height that can be used to create a grid with equally sized boxes.
/// </summary>
/// <param name="parent">The element the groups should be added to, can be <see langword="null"/>.</param>
/// <param name="totalSize">The total size that the grid should take up.</param>
/// <param name="width">The width of the grid, or the amount of columns it should have.</param>
/// <param name="height">The height of the grid, or the amount of rows it should have.</param>
/// <returns>The created grid.</returns>
public static Group [ , ] MakeGrid ( Element parent , Vector2 totalSize , int width , int height ) {
var grid = new Group [ width , height ] ;
for ( var y = 0 ; y < height ; y + + ) {
for ( var x = 0 ; x < width ; x + + ) {
var anchor = x = = 0 ? Anchor . AutoLeft : Anchor . AutoInlineIgnoreOverflow ;
grid [ x , y ] = new Group ( anchor , new Vector2 ( totalSize . X / width , totalSize . Y / height ) , false ) ;
parent ? . AddChild ( grid [ x , y ] ) ;
}
}
return grid ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a <see cref="TextField"/> with a + and a - button next to it, to allow for easy number input.
/// </summary>
/// <param name="anchor">The text field's anchor</param>
/// <param name="size">The size of the text field</param>
/// <param name="defaultValue">The default content of the text field</param>
/// <param name="stepPerClick">The value that is added or removed to the text field's value when clicking the + or - buttons</param>
/// <param name="rule">The rule for text input. <see cref="TextField.OnlyNumbers"/> by default.</param>
/// <param name="onTextChange">A callback that is invoked when the text field's text changes</param>
/// <returns>A group that contains the number field</returns>
2019-08-24 12:40:20 +02:00
public static Group NumberField ( Anchor anchor , Vector2 size , int defaultValue = 0 , int stepPerClick = 1 , TextField . Rule rule = null , TextField . TextChanged onTextChange = null ) {
var group = new Group ( anchor , size , false ) ;
var field = new TextField ( Anchor . TopLeft , Vector2 . One , rule ? ? TextField . OnlyNumbers ) ;
field . OnTextChange = onTextChange ;
2019-09-05 12:51:40 +02:00
field . SetText ( defaultValue . ToString ( ) ) ;
2019-08-24 12:40:20 +02:00
group . AddChild ( field ) ;
group . OnAreaUpdated + = e = > field . Size = new Vector2 ( ( e . Area . Width - e . Area . Height / 2 ) / e . Scale , 1 ) ;
var upButton = new Button ( Anchor . TopRight , Vector2 . One , "+" ) {
2019-08-25 21:49:27 +02:00
OnPressed = element = > {
2020-06-18 17:24:35 +02:00
var text = field . Text ;
2019-08-25 21:49:27 +02:00
if ( int . TryParse ( text , out var val ) )
field . SetText ( val + stepPerClick ) ;
2019-08-24 12:40:20 +02:00
}
} ;
group . AddChild ( upButton ) ;
group . OnAreaUpdated + = e = > upButton . Size = new Vector2 ( e . Area . Height / 2 / e . Scale ) ;
var downButton = new Button ( Anchor . BottomRight , Vector2 . One , "-" ) {
2019-08-25 21:49:27 +02:00
OnPressed = element = > {
2020-06-18 17:24:35 +02:00
var text = field . Text ;
2019-08-25 21:49:27 +02:00
if ( int . TryParse ( text , out var val ) )
field . SetText ( val - stepPerClick ) ;
2019-08-24 12:40:20 +02:00
}
} ;
group . AddChild ( downButton ) ;
group . OnAreaUpdated + = e = > downButton . Size = new Vector2 ( e . Area . Height / 2 / e . Scale ) ;
return group ;
}
2022-05-18 16:01:24 +02:00
/// <inheritdoc cref="KeybindButton(MLEM.Ui.Anchor,Microsoft.Xna.Framework.Vector2,MLEM.Input.Keybind,MLEM.Input.InputHandler,string,MLEM.Input.Keybind,string,System.Func{MLEM.Input.GenericInput,string},int,System.Func{MLEM.Input.GenericInput,System.Collections.Generic.IEnumerable{MLEM.Input.GenericInput},bool})"/>
2022-05-18 15:54:29 +02:00
public static Button KeybindButton ( Anchor anchor , Vector2 size , Keybind keybind , InputHandler inputHandler , string activePlaceholder , GenericInput unbindKey = default , string unboundPlaceholder = "" , Func < GenericInput , string > inputName = null , int index = 0 , Func < GenericInput , IEnumerable < GenericInput > , bool > isKeybindAllowed = null ) {
2022-06-15 11:38:11 +02:00
return ElementHelper . KeybindButton ( anchor , size , keybind , inputHandler , activePlaceholder , new Keybind ( unbindKey ) , unboundPlaceholder , inputName , index , isKeybindAllowed ) ;
2022-03-26 12:41:19 +01:00
}
2021-06-21 00:49:09 +02:00
/// <summary>
/// Creates a <see cref="Button"/> that acts as a way to input a custom value for a <see cref="Keybind"/>.
2022-03-10 12:39:56 +01:00
/// Note that only the <see cref="Keybind.Combination"/> at index <paramref name="index"/> of the given keybind is displayed and edited, all others are ignored.
2021-06-21 00:49:09 +02:00
/// Inputting custom keybinds using this element supports <see cref="ModifierKey"/>-based modifiers and any <see cref="GenericInput"/>-based keys.
2022-03-10 13:08:49 +01:00
/// The keybind button's current state can be retrieved using the "Active" <see cref="GenericDataHolder.GetData{T}"/> key, which stores a bool that indicates whether the button is currently waiting for a new value to be assigned.
2021-06-21 00:49:09 +02:00
/// </summary>
/// <param name="anchor">The button's anchor</param>
/// <param name="size">The button's size</param>
/// <param name="keybind">The keybind that this button should represent</param>
/// <param name="inputHandler">The input handler to query inputs with</param>
/// <param name="activePlaceholder">A placeholder text that is displayed while the keybind is being edited</param>
2022-03-26 12:41:19 +01:00
/// <param name="unbind">An optional keybind to press that allows the keybind value to be unbound, clearing the combination</param>
2021-06-21 00:49:09 +02:00
/// <param name="unboundPlaceholder">A placeholder text that is displayed if the keybind is unbound</param>
/// <param name="inputName">An optional function to give each input a display name that is easier to read. If this is null, <see cref="GenericInput.ToString"/> is used.</param>
2022-03-10 12:39:56 +01:00
/// <param name="index">The index in the <paramref name="keybind"/> that the button should display. Note that, if the index is greater than the amount of combinations, combinations entered using this button will be stored at an earlier index.</param>
2022-05-18 15:54:29 +02:00
/// <param name="isKeybindAllowed">A function that can optionally determine whether a given input and modifier combination is allowed. If this is null, all combinations are allowed.</param>
2021-06-21 00:49:09 +02:00
/// <returns>A keybind button with the given settings</returns>
2022-05-18 15:54:29 +02:00
public static Button KeybindButton ( Anchor anchor , Vector2 size , Keybind keybind , InputHandler inputHandler , string activePlaceholder , Keybind unbind = default , string unboundPlaceholder = "" , Func < GenericInput , string > inputName = null , int index = 0 , Func < GenericInput , IEnumerable < GenericInput > , bool > isKeybindAllowed = null ) {
2022-06-15 11:38:11 +02:00
string GetCurrentName ( ) {
return keybind . TryGetCombination ( index , out var combination ) ? combination . ToString ( " + " , inputName ) : unboundPlaceholder ;
}
2021-06-21 00:49:09 +02:00
var button = new Button ( anchor , size , GetCurrentName ( ) ) ;
var activeNext = false ;
button . OnPressed = e = > {
button . Text . Text = activePlaceholder ;
activeNext = true ;
} ;
button . OnUpdated = ( e , time ) = > {
if ( activeNext ) {
2022-03-10 13:08:49 +01:00
button . SetData ( "Active" , true ) ;
2021-06-21 00:49:09 +02:00
activeNext = false ;
2022-03-10 13:08:49 +01:00
} else if ( button . GetData < bool > ( "Active" ) ) {
2022-04-30 12:26:40 +02:00
if ( unbind ! = null & & unbind . TryConsumePressed ( inputHandler ) ) {
2022-03-10 12:39:56 +01:00
keybind . Remove ( ( c , i ) = > i = = index ) ;
2021-06-21 00:49:09 +02:00
button . Text . Text = unboundPlaceholder ;
2022-03-10 13:08:49 +01:00
button . SetData ( "Active" , false ) ;
2021-06-21 00:49:09 +02:00
} else if ( inputHandler . InputsPressed . Length > 0 ) {
var key = inputHandler . InputsPressed . FirstOrDefault ( i = > ! i . IsModifier ( ) ) ;
if ( key ! = default ) {
2022-05-18 15:54:29 +02:00
var mods = inputHandler . InputsDown . Where ( i = > i . IsModifier ( ) ) . ToArray ( ) ;
if ( isKeybindAllowed = = null | | isKeybindAllowed ( key , mods ) ) {
keybind . Remove ( ( c , i ) = > i = = index ) . Insert ( index , key , mods ) ;
button . Text . Text = GetCurrentName ( ) ;
button . SetData ( "Active" , false ) ;
}
2021-06-21 00:49:09 +02:00
}
}
2022-03-10 12:39:56 +01:00
} else {
button . Text . Text = GetCurrentName ( ) ;
2021-06-21 00:49:09 +02:00
}
} ;
return button ;
}
2019-08-24 12:40:20 +02:00
}
2021-02-18 04:16:17 +01:00
/// <summary>
/// This class contains a set of extensions for dealing with <see cref="Element"/> objects
/// </summary>
public static class ElementExtensions {
/// <summary>
2021-10-29 23:33:15 +02:00
/// Adds a new <see cref="Tooltip"/> to the given element using the <see cref="Tooltip(Paragraph.TextCallback,Element)"/> constructor
2021-02-18 04:16:17 +01:00
/// </summary>
2021-10-29 23:33:15 +02:00
/// <param name="element">The element to add the tooltip to</param>
2021-02-18 04:16:17 +01:00
/// <param name="textCallback">The text to display on the tooltip</param>
/// <returns>The created tooltip instance</returns>
2021-10-29 23:33:15 +02:00
public static Tooltip AddTooltip ( this Element element , Paragraph . TextCallback textCallback ) {
2022-05-03 20:10:26 +02:00
return element . AddTooltip ( new Tooltip ( textCallback ) ) ;
2021-02-18 04:16:17 +01:00
}
/// <summary>
2021-10-29 23:33:15 +02:00
/// Adds a new <see cref="Tooltip"/> to the given element using the <see cref="Tooltip(string,Element)"/> constructor
2021-02-18 04:16:17 +01:00
/// </summary>
2021-10-29 23:33:15 +02:00
/// <param name="element">The element to add the tooltip to</param>
2021-02-18 04:16:17 +01:00
/// <param name="text">The text to display on the tooltip</param>
/// <returns>The created tooltip instance</returns>
2021-10-29 23:33:15 +02:00
public static Tooltip AddTooltip ( this Element element , string text ) {
2022-05-03 20:10:26 +02:00
return element . AddTooltip ( new Tooltip ( text ) ) ;
}
/// <summary>
/// Adds the given <see cref="Tooltip"/> to the given element
/// </summary>
/// <param name="element">The element to add the tooltip to</param>
/// <param name="tooltip">The tooltip to add</param>
/// <returns>The passed tooltip, for chaining</returns>
public static Tooltip AddTooltip ( this Element element , Tooltip tooltip ) {
tooltip . AddToElement ( element ) ;
return tooltip ;
2021-02-18 04:16:17 +01:00
}
2022-07-27 11:19:40 +02:00
/// <summary>
/// Returns whether the given <see cref="Anchor"/> is automatic. The anchors <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoCenter"/>, <see cref="Anchor.AutoRight"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
/// </summary>
/// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is automatic.</returns>
public static bool IsAuto ( this Anchor anchor ) {
return anchor = = Anchor . AutoLeft | | anchor = = Anchor . AutoCenter | | anchor = = Anchor . AutoRight | | anchor = = Anchor . AutoInline | | anchor = = Anchor . AutoInlineIgnoreOverflow ;
}
/// <summary>
/// Returns whether the given <see cref="Anchor"/> is left-aligned for the purpose of <see cref="Element.GetRightmostChild"/>. The anchors <see cref="Anchor.TopLeft"/>, <see cref="Anchor.CenterLeft"/>, <see cref="Anchor.BottomLeft"/>, <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
/// </summary>
/// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is left-aligned.</returns>
public static bool IsLeftAligned ( this Anchor anchor ) {
return anchor = = Anchor . TopLeft | | anchor = = Anchor . CenterLeft | | anchor = = Anchor . BottomLeft | | anchor = = Anchor . AutoLeft | | anchor = = Anchor . AutoInline | | anchor = = Anchor . AutoInlineIgnoreOverflow ;
}
/// <summary>
/// Returns whether the given <see cref="Anchor"/> is top-aligned for the purpose of <see cref="Element.GetLowestChild"/>. The anchors <see cref="Anchor.TopLeft"/>, <see cref="Anchor.TopCenter"/>, <see cref="Anchor.TopRight"/>, <see cref="Anchor.AutoLeft"/>, <see cref="Anchor.AutoCenter"/>, <see cref="Anchor.AutoRight"/>, <see cref="Anchor.AutoInline"/> and <see cref="Anchor.AutoInlineIgnoreOverflow"/> will return true.
/// </summary>
/// <param name="anchor">The anchor to query.</param>
/// <returns>Whether the given anchor is top-aligned.</returns>
public static bool IsTopAligned ( this Anchor anchor ) {
return anchor = = Anchor . TopLeft | | anchor = = Anchor . TopCenter | | anchor = = Anchor . TopRight | | anchor = = Anchor . AutoLeft | | anchor = = Anchor . AutoCenter | | anchor = = Anchor . AutoRight | | anchor = = Anchor . AutoInline | | anchor = = Anchor . AutoInlineIgnoreOverflow ;
}
2021-02-18 04:16:17 +01:00
}
2022-06-17 18:23:47 +02:00
}