1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-19 23:51:23 +02:00

full xml documentation for MLEM.Ui

This commit is contained in:
Ellpeck 2020-05-22 17:02:24 +02:00
parent 1729b56f28
commit f12284169e
24 changed files with 1560 additions and 14 deletions

View file

@ -1,21 +1,74 @@
using MLEM.Ui.Elements;
namespace MLEM.Ui {
/// <summary>
/// Represents a location for an <see cref="Element"/> to attach to within its parent (or within the screen's viewport if it is the <see cref="RootElement"/>).
/// </summary>
public enum Anchor {
/// <summary>
/// Attach to the top left corner of the parent
/// </summary>
TopLeft,
/// <summary>
/// Attach to the center of the top edge of the parent
/// </summary>
TopCenter,
/// <summary>
/// Attach to the top right corner of the parent
/// </summary>
TopRight,
/// <summary>
/// Attach to the center of the left edge of the parent
/// </summary>
CenterLeft,
/// <summary>
/// Attach to the center position of the parent
/// </summary>
Center,
/// <summary>
/// Attach to the center of the right edge of the parent
/// </summary>
CenterRight,
/// <summary>
/// Attach to the bottom left corner of the parent
/// </summary>
BottomLeft,
/// <summary>
/// Attach to the center of the bottom edge of the parent
/// </summary>
BottomCenter,
/// <summary>
/// Attach to the bottom right corner of the parent
/// </summary>
BottomRight,
AutoLeft, // below older sibling, aligned to the left
AutoCenter, // below older sibling, aligned to the center
AutoRight, //below older sibling, aligned to the right
AutoInline, // right of older sibling or below if overflows
AutoInlineIgnoreOverflow // right of older sibling at all time
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed below its older sibling, aligned to the left edge of its parent.
/// </summary>
AutoLeft,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed below its older sibling, aligned to the horizontal center of its parent.
/// </summary>
AutoCenter,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed below its older sibling, aligned to the right edge of its parent.
/// </summary>
AutoRight,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor will cause an element to be placed in the same line as its older sibling, or at the start of the next line if there is no space to the right of its older sibling.
/// </summary>
AutoInline,
/// <summary>
/// This is an auto-anchoring value.
/// This anchor is an overflow-ignoring version of <see cref="AutoInline"/>, meaning that the element will never be forced into the next line.
/// Note that, when using this property, it is very easy to cause an element to overflow out of its parent container.
/// </summary>
AutoInlineIgnoreOverflow
}
}

View file

@ -5,18 +5,54 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A button element for use inside of a <see cref="UiSystem"/>.
/// A button element can be pressed, hovered over and that can be disabled.
/// </summary>
public class Button : Element {
/// <summary>
/// The button's texture
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// The color that the button draws its texture with
/// </summary>
public StyleProp<Color> NormalColor = Color.White;
/// <summary>
/// The texture that the button uses while being moused over.
/// If this is null, it uses its default <see cref="Texture"/>.
/// </summary>
public StyleProp<NinePatch> HoveredTexture;
/// <summary>
/// The color that the button uses for drawing while being moused over
/// </summary>
public StyleProp<Color> HoveredColor;
/// <summary>
/// The texture that the button uses when it <see cref="IsDisabled"/>.
/// If this is null, it uses its default <see cref="Texture"/>.
/// </summary>
public StyleProp<NinePatch> DisabledTexture;
/// <summary>
/// The color that the button uses for drawing when it <see cref="IsDisabled"/>
/// </summary>
public StyleProp<Color> DisabledColor;
/// <summary>
/// The <see cref="Paragraph"/> of text that is displayed on the button.
/// Note that this is only nonnull by default if the constructor was passed a nonnull text.
/// </summary>
public Paragraph Text;
/// <summary>
/// The <see cref="Tooltip"/> that is displayed when hovering over the button.
/// Note that this is only nonnull by default if the constructor was passed a nonnull tooltip text.
/// </summary>
public Tooltip Tooltip;
private bool isDisabled;
/// <summary>
/// Set this property to true to mark the button as disabled.
/// A disabled button cannot be moused over, selected or pressed.
/// </summary>
public bool IsDisabled {
get => this.isDisabled;
set {
@ -26,6 +62,14 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Creates a new button with the given settings
/// </summary>
/// <param name="anchor">The button's anchor</param>
/// <param name="size">The button's size</param>
/// <param name="text">The text that should be displayed on the button</param>
/// <param name="tooltipText">The text that should be displayed in a <see cref="Tooltip"/> when hovering over this button</param>
/// <param name="tooltipWidth">The width of this button's <see cref="Tooltip"/>, or 50 by default</param>
public Button(Anchor anchor, Vector2 size, string text = null, string tooltipText = null, float tooltipWidth = 50) : base(anchor, size) {
if (text != null) {
this.Text = new Paragraph(Anchor.Center, 1, text, true);
@ -35,6 +79,7 @@ namespace MLEM.Ui.Elements {
this.Tooltip = new Tooltip(tooltipWidth, tooltipText, this);
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var tex = this.Texture;
var color = (Color) this.NormalColor * alpha;
@ -49,6 +94,7 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.ButtonTexture);

View file

@ -7,16 +7,43 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A checkbox element to use inside of a <see cref="UiSystem"/>.
/// A checkbox can be checked by pressing it and will stay checked until it is pressed again.
/// For a checkbox that causes neighboring checkboxes to be deselected automatically, use <see cref="RadioButton"/>.
/// </summary>
public class Checkbox : Element {
/// <summary>
/// The texture that this checkbox uses for drawing
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// The texture that this checkbox uses when it is hovered.
/// If this is null, the default <see cref="Texture"/> is used.
/// </summary>
public StyleProp<NinePatch> HoveredTexture;
/// <summary>
/// The color that this checkbox uses for drawing when it is hovered.
/// </summary>
public StyleProp<Color> HoveredColor;
/// <summary>
/// The texture that is rendered on top of this checkbox when it is <see cref="Checked"/>.
/// </summary>
public StyleProp<TextureRegion> Checkmark;
/// <summary>
/// The label <see cref="Paragraph"/> that displays next to this checkbox
/// </summary>
public Paragraph Label;
/// <summary>
/// The width of the space between this checkbox and its <see cref="Label"/>
/// </summary>
public float TextOffsetX = 2;
private bool checced;
/// <summary>
/// Whether or not this checkbox is currently checked.
/// </summary>
public bool Checked {
get => this.checced;
set {
@ -26,8 +53,18 @@ namespace MLEM.Ui.Elements {
}
}
}
/// <summary>
/// An event that is invoked when this checkbox's <see cref="Checked"/> property changes
/// </summary>
public CheckStateChange OnCheckStateChange;
/// <summary>
/// Creates a new checkbox with the given settings
/// </summary>
/// <param name="anchor">The checkbox's anchor</param>
/// <param name="size">The checkbox's size</param>
/// <param name="label">The checkbox's label text</param>
/// <param name="defaultChecked">The default value of <see cref="Checked"/></param>
public Checkbox(Anchor anchor, Vector2 size, string label, bool defaultChecked = false) : base(anchor, size) {
this.checced = defaultChecked;
this.OnPressed += element => this.Checked = !this.Checked;
@ -38,6 +75,7 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
protected override Vector2 CalcActualSize(RectangleF parentArea) {
var size = base.CalcActualSize(parentArea);
if (this.Label != null) {
@ -47,6 +85,7 @@ namespace MLEM.Ui.Elements {
return size;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var tex = this.Texture;
var color = Color.White * alpha;
@ -62,6 +101,7 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.CheckboxTexture);
@ -70,6 +110,11 @@ namespace MLEM.Ui.Elements {
this.Checkmark.SetFromStyle(style.CheckboxCheckmark);
}
/// <summary>
/// A delegate used for <see cref="Checkbox.OnCheckStateChange"/>
/// </summary>
/// <param name="box">The checkbox whose checked state changed</param>
/// <param name="checced">The new value of <see cref="Checkbox.Checked"/></param>
public delegate void CheckStateChange(Checkbox box, bool checced);
}

View file

@ -3,12 +3,26 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Ui.Elements {
/// <summary>
/// A <see cref="Group"/> that can have custom drawing parameters.
/// Custom drawing parameters include a <see cref="Transform"/> matrix, as well as a custom <see cref="SpriteBatch.Begin"/> call.
/// All <see cref="Element.Children"/> of the custom draw group will be drawn with the custom parameters.
/// </summary>
public class CustomDrawGroup : Group {
/// <summary>
/// This custom draw group's transform matrix
/// </summary>
public Matrix? Transform;
/// <summary>
/// A callback for retrieving this group's <see cref="Transform"/> automatically
/// </summary>
public TransformCallback TransformGetter;
private BeginDelegate beginImpl;
private bool isDefaultBegin;
/// <summary>
/// The call that this custom draw group should make to <see cref="SpriteBatch"/> to begin drawing.
/// </summary>
public BeginDelegate BeginImpl {
get => this.beginImpl;
set {
@ -17,12 +31,21 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Creates a new custom draw group with the given settings
/// </summary>
/// <param name="anchor">The group's anchor</param>
/// <param name="size">The group's size</param>
/// <param name="transformGetter">The group's <see cref="TransformGetter"/></param>
/// <param name="beginImpl">The group's <see cref="BeginImpl"/></param>
/// <param name="setHeightBasedOnChildren">Whether this group should automatically calculate its height based on its children</param>
public CustomDrawGroup(Anchor anchor, Vector2 size, TransformCallback transformGetter = null, BeginDelegate beginImpl = null, bool setHeightBasedOnChildren = true) :
base(anchor, size, setHeightBasedOnChildren) {
this.TransformGetter = transformGetter ?? ((element, time, matrix) => Matrix.Identity);
this.BeginImpl = beginImpl;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var transform = this.Transform ?? this.TransformGetter(this, time, matrix);
var customDraw = !this.isDefaultBegin || transform != Matrix.Identity;
@ -42,12 +65,33 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Scales this custom draw group'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 ScaleOrigin(float scale, Vector2? origin = null) {
this.Transform = Matrix.CreateScale(scale, scale, 0) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.DisplayArea.Center), 0));
}
/// <summary>
/// A delegate method used for <see cref="CustomDrawGroup.BeginImpl"/>
/// </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>
/// <param name="matrix">The transform matrix used for drawing</param>
public delegate void BeginDelegate(CustomDrawGroup element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix);
/// <summary>
/// A delegate method used for <see cref="CustomDrawGroup.TransformGetter"/>
/// </summary>
/// <param name="element">The element whose transform to get</param>
/// <param name="time">The game's time</param>
/// <param name="matrix">The regular transform matrix</param>
public delegate Matrix TransformCallback(CustomDrawGroup element, GameTime time, Matrix matrix);
}

View file

@ -3,9 +3,19 @@ using Microsoft.Xna.Framework;
using MLEM.Misc;
namespace MLEM.Ui.Elements {
/// <summary>
/// A dropdown component to use inside of a <see cref="UiSystem"/>.
/// A dropdown is a component that contains a hidden panel which is displayed upon pressing the dropdown button.
/// </summary>
public class Dropdown : Button {
/// <summary>
/// The panel that this dropdown contains. It will be displayed upon pressing the dropdown button.
/// </summary>
public readonly Panel Panel;
/// <summary>
/// This property stores whether the dropdown is currently opened or not
/// </summary>
public bool IsOpen {
get => !this.Panel.IsHidden;
set {
@ -13,8 +23,19 @@ namespace MLEM.Ui.Elements {
this.OnOpenedOrClosed?.Invoke(this);
}
}
/// <summary>
/// An event that is invoked when <see cref="IsOpen"/> changes
/// </summary>
public GenericCallback OnOpenedOrClosed;
/// <summary>
/// Creates a new dropdown with the given settings
/// </summary>
/// <param name="anchor">The dropdown's anchor</param>
/// <param name="size">The dropdown button's size</param>
/// <param name="text">The text displayed on the dropdown button</param>
/// <param name="tooltipText">The text displayed as a tooltip when hovering over the dropdown button</param>
/// <param name="tooltipWidth">The width of the dropdown button's tooltip</param>
public Dropdown(Anchor anchor, Vector2 size, string text = null, string tooltipText = null, float tooltipWidth = 50) : base(anchor, size, text, tooltipText, tooltipWidth) {
this.Panel = this.AddChild(new Panel(Anchor.TopCenter, size, Vector2.Zero, true) {
IsHidden = true
@ -24,6 +45,10 @@ namespace MLEM.Ui.Elements {
this.OnPressed += e => this.IsOpen = !this.IsOpen;
}
/// <summary>
/// Adds an element to this dropdown's <see cref="Panel"/>
/// </summary>
/// <param name="element">The element to add</param>
public void AddElement(Element element) {
this.Panel.AddChild(element);
// Since the dropdown causes elements to be over each other,
@ -39,10 +64,21 @@ namespace MLEM.Ui.Elements {
};
}
/// <summary>
/// Adds a pressable <see cref="Paragraph"/> element to this dropdown's <see cref="Panel"/>
/// </summary>
/// <param name="text">The text to display</param>
/// <param name="pressed">The resulting paragraph's <see cref="Element.OnPressed"/> event</param>
public void AddElement(string text, GenericCallback pressed = null) {
this.AddElement(p => text, pressed);
}
/// <summary>
/// Adds a pressable <see cref="Paragraph"/> element to this dropdown's <see cref="Panel"/>.
/// By default, the paragraph's text color will change from <see cref="Color.White"/> to <see cref="Color.LightGray"/> when hovering over it.
/// </summary>
/// <param name="text">The text to display</param>
/// <param name="pressed">The resulting paragraph's <see cref="Element.OnPressed"/> event</param>
public void AddElement(Paragraph.TextCallback text, GenericCallback pressed = null) {
var paragraph = new Paragraph(Anchor.AutoLeft, 1, text) {
CanBeMoused = true,

View file

@ -13,10 +13,20 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// This class represents a generic base class for ui elements of a <see cref="UiSystem"/>.
/// </summary>
public abstract class Element : GenericDataHolder {
/// <summary>
/// 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.
/// </summary>
protected readonly List<Element> Children = new List<Element>();
private readonly List<Element> sortedChildren = new List<Element>();
/// <summary>
/// A sorted version of <see cref="Children"/>. The children are sorted by their <see cref="Priority"/>.
/// </summary>
protected List<Element> SortedChildren {
get {
this.UpdateSortedChildrenIfDirty();
@ -26,6 +36,9 @@ namespace MLEM.Ui.Elements {
private bool sortedChildrenDirty;
private UiSystem system;
/// <summary>
/// The ui system that this element is currently a part of
/// </summary>
public UiSystem System {
get => this.system;
internal set {
@ -35,13 +48,33 @@ namespace MLEM.Ui.Elements {
this.InitStyle(this.system.Style);
}
}
/// <summary>
/// The controls that this element's <see cref="System"/> uses
/// </summary>
public UiControls Controls;
/// <summary>
/// The input handler that this element's <see cref="Controls"/> use
/// </summary>
protected InputHandler Input => this.Controls.Input;
/// <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>
public Element Parent { get; private set; }
/// <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>
public RootElement Root { get; internal set; }
/// <summary>
/// The scale that this ui element renders with
/// </summary>
public float Scale => this.Root.ActualScale;
private Anchor anchor;
/// <summary>
/// The <see cref="Anchor"/> that this element uses for positioning within its parent
/// </summary>
public Anchor Anchor {
get => this.anchor;
set {
@ -53,6 +86,10 @@ namespace MLEM.Ui.Elements {
}
private Vector2 size;
/// <summary>
/// The size of this element.
/// If the x or y value of the size is lower than or equal to 1, the size will be seen as a percentage of its parent's size.
/// </summary>
public Vector2 Size {
get => this.size;
set {
@ -62,9 +99,16 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
}
/// <summary>
/// The <see cref="Size"/>, but with <see cref="Scale"/> applied.
/// </summary>
public Vector2 ScaledSize => this.size * this.Scale;
private Vector2 offset;
/// <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>
public Vector2 PositionOffset {
get => this.offset;
set {
@ -74,12 +118,26 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
}
/// <summary>
/// The <see cref="PositionOffset"/>, but with <see cref="Scale"/> applied.
/// </summary>
public Vector2 ScaledOffset => this.offset * this.Scale;
/// <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 Padding Padding;
/// <summary>
/// The <see cref="Padding"/>, but with <see cref="Scale"/> applied.
/// </summary>
public Padding ScaledPadding => this.Padding * this.Scale;
private Padding childPadding;
/// <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>
public Padding ChildPadding {
get => this.childPadding;
set {
@ -89,10 +147,20 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
}
/// <summary>
/// The <see cref="ChildPadding"/>, but with <see cref="Scale"/> applied.
/// </summary>
public Padding ScaledChildPadding => this.childPadding * this.Scale;
/// <summary>
/// This element's current <see cref="Area"/>, but with <see cref="ScaledChildPadding"/> applied.
/// </summary>
public RectangleF ChildPaddedArea => this.UnscrolledArea.Shrink(this.ScaledChildPadding);
private RectangleF area;
/// <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>
public RectangleF UnscrolledArea {
get {
this.UpdateAreaIfDirty();
@ -100,13 +168,30 @@ namespace MLEM.Ui.Elements {
}
}
private bool areaDirty;
/// <summary>
/// The <see cref="UnscrolledArea"/> of this element, but with <see cref="ScaledScrollOffset"/> applied.
/// </summary>
public RectangleF Area => this.UnscrolledArea.OffsetCopy(this.ScaledScrollOffset);
/// <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>
public RectangleF DisplayArea => this.Area.Shrink(this.ScaledPadding);
/// <summary>
/// The offset that this element has as a result of <see cref="Panel"/> scrolling.
/// </summary>
public Vector2 ScrollOffset;
/// <summary>
/// The <see cref="ScrollOffset"/>, but with <see cref="Scale"/> applied.
/// </summary>
public Vector2 ScaledScrollOffset => this.ScrollOffset * this.Scale;
private bool isHidden;
/// <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>
public bool IsHidden {
get => this.isHidden;
set {
@ -118,6 +203,10 @@ namespace MLEM.Ui.Elements {
}
private int priority;
/// <summary>
/// The priority of this element as part of its <see cref="Parent"/> element.
/// A higher priority means the element will be drawn first and, if auto-anchoring is used, anchored higher up within its parent.
/// </summary>
public int Priority {
get => this.priority;
set {
@ -127,39 +216,136 @@ namespace MLEM.Ui.Elements {
}
}
/// <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>
public bool CanBeSelected = true;
/// <summary>
/// Set this field to false to disallow the element from reacting to being moused over.
/// </summary>
public bool CanBeMoused = true;
/// <summary>
/// Set this field to false to disallow this element's <see cref="OnPressed"/> and <see cref="OnSecondaryPressed"/> events to be called.
/// </summary>
public bool CanBePressed = true;
/// <summary>
/// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point.
/// </summary>
public bool CanAutoAnchorsAttach = true;
/// <summary>
/// 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.
/// </summary>
public bool SetHeightBasedOnChildren;
/// <summary>
/// Set this field to true to cause this element's width to be automatically calculated based on the area that is <see cref="Children"/> take up.
/// </summary>
public bool SetWidthBasedOnChildren;
/// <summary>
/// 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>
public float DrawAlpha = 1;
/// <summary>
/// Stores whether this element is currently being moused over.
/// </summary>
public bool IsMouseOver { get; private set; }
/// <summary>
/// Stores whether this element is its <see cref="Root"/>'s <see cref="RootElement.SelectedElement"/>.
/// </summary>
public bool IsSelected { get; private set; }
/// <summary>
/// Event that is called after this element is drawn, but before its children are drawn
/// </summary>
public DrawCallback OnDrawn;
/// <summary>
/// Event that is called when this element is updated
/// </summary>
public TimeCallback OnUpdated;
/// <summary>
/// Event that is called when this element is pressed
/// </summary>
public GenericCallback OnPressed;
/// <summary>
/// Event that is called when this element is pressed using the secondary action
/// </summary>
public GenericCallback OnSecondaryPressed;
/// <summary>
/// Event that is called when this element's <see cref="IsSelected"/> is turned true
/// </summary>
public GenericCallback OnSelected;
/// <summary>
/// Event that is called when this element's <see cref="IsSelected"/> is turned false
/// </summary>
public GenericCallback OnDeselected;
/// <summary>
/// Event that is called when this element starts being moused over
/// </summary>
public GenericCallback OnMouseEnter;
/// <summary>
/// Event that is called when this element stops being moused over
/// </summary>
public GenericCallback OnMouseExit;
/// <summary>
/// Event that is called when text input is made.
/// Note that this event is called for every element, even if it is not selected.
/// Also note that if <see cref="TextInputWrapper.RequiresOnScreenKeyboard"/> is true, this event is never called.
/// </summary>
public TextInputCallback OnTextInput;
/// <summary>
/// Event that is called when this element's <see cref="DisplayArea"/> is changed.
/// </summary>
public GenericCallback OnAreaUpdated;
/// <summary>
/// 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>
public OtherElementCallback OnMousedElementChanged;
/// <summary>
/// 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>
public OtherElementCallback OnSelectedElementChanged;
/// <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>
public TabNextElementCallback GetTabNextElement;
/// <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>
public GamepadNextElementCallback GetGamepadNextElement;
/// <summary>
/// Event that is called when a child is added to this element using <see cref="AddChild{T}"/>
/// </summary>
public OtherElementCallback OnChildAdded;
/// <summary>
/// Event that is called when a child is removed from this element using <see cref="RemoveChild"/>
/// </summary>
public OtherElementCallback OnChildRemoved;
/// <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<SoundEffectInstance> 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<SoundEffectInstance> SecondActionSound;
public Element(Anchor anchor, Vector2 size) {
/// <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) {
this.anchor = anchor;
this.size = size;
@ -173,6 +359,13 @@ namespace MLEM.Ui.Elements {
this.SetAreaDirty();
}
/// <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>
public T AddChild<T>(T element, int index = -1) where T : Element {
if (index < 0 || index > this.Children.Count)
index = this.Children.Count;
@ -189,6 +382,10 @@ namespace MLEM.Ui.Elements {
return element;
}
/// <summary>
/// Removes the given child from this element.
/// </summary>
/// <param name="element">The child element to remove</param>
public void RemoveChild(Element element) {
this.Children.Remove(element);
// set area dirty here so that a dirty call is made
@ -204,6 +401,10 @@ namespace MLEM.Ui.Elements {
this.SetSortedChildrenDirty();
}
/// <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>
public void RemoveChildren(Func<Element, bool> condition = null) {
for (var i = this.Children.Count - 1; i >= 0; i--) {
var child = this.Children[i];
@ -213,15 +414,24 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Causes <see cref="SortedChildren"/> to be recalculated as soon as possible.
/// </summary>
public void SetSortedChildrenDirty() {
this.sortedChildrenDirty = true;
}
/// <summary>
/// Updates the <see cref="SortedChildren"/> list if <see cref="SetSortedChildrenDirty"/> is true.
/// </summary>
public void UpdateSortedChildrenIfDirty() {
if (this.sortedChildrenDirty)
this.ForceUpdateSortedChildren();
}
/// <summary>
/// Forces an update of the <see cref="SortedChildren"/> list.
/// </summary>
public virtual void ForceUpdateSortedChildren() {
this.sortedChildrenDirty = false;
@ -230,17 +440,28 @@ namespace MLEM.Ui.Elements {
this.sortedChildren.Sort((e1, e2) => e1.Priority.CompareTo(e2.Priority));
}
/// <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>
public void SetAreaDirty() {
this.areaDirty = true;
if (this.Parent != null && (this.Anchor >= Anchor.AutoLeft || this.Parent.SetWidthBasedOnChildren || this.Parent.SetHeightBasedOnChildren))
this.Parent.SetAreaDirty();
}
/// <summary>
/// Updates this element's <see cref="Area"/> list if <see cref="areaDirty"/> is true.
/// </summary>
public void UpdateAreaIfDirty() {
if (this.areaDirty)
this.ForceUpdateArea();
}
/// <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>
public virtual void ForceUpdateArea() {
this.areaDirty = false;
if (this.IsHidden)
@ -362,16 +583,30 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Calculates the actual size that this element should take up, based on the area that its parent encompasses.
/// </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>
protected virtual Vector2 CalcActualSize(RectangleF parentArea) {
return new Vector2(
this.size.X > 1 ? this.ScaledSize.X : parentArea.Width * this.size.X,
this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y);
}
/// <summary>
/// Returns the area that should be used for determining where auto-anchoring children should attach.
/// </summary>
/// <returns>The area for auto anchors</returns>
protected virtual RectangleF GetAreaForAutoAnchors() {
return this.UnscrolledArea;
}
/// <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>
public Element GetLowestChild(Func<Element, bool> condition = null) {
Element lowest = null;
foreach (var child in this.Children) {
@ -385,6 +620,11 @@ namespace MLEM.Ui.Elements {
return lowest;
}
/// <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>
public Element GetRightmostChild(Func<Element, bool> condition = null) {
Element rightmost = null;
foreach (var child in this.Children) {
@ -398,6 +638,12 @@ namespace MLEM.Ui.Elements {
return rightmost;
}
/// <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>
public Element GetLowestOlderSibling(Func<Element, bool> condition = null) {
if (this.Parent == null)
return null;
@ -413,6 +659,12 @@ namespace MLEM.Ui.Elements {
return lowest;
}
/// <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>
public Element GetOlderSibling(Func<Element, bool> condition = null) {
if (this.Parent == null)
return null;
@ -427,6 +679,12 @@ namespace MLEM.Ui.Elements {
return older;
}
/// <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>
public IEnumerable<Element> GetSiblings(Func<Element, bool> condition = null) {
if (this.Parent == null)
yield break;
@ -438,6 +696,15 @@ namespace MLEM.Ui.Elements {
}
}
/// <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>
public IEnumerable<T> GetChildren<T>(Func<T, bool> condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) where T : Element {
foreach (var child in this.Children) {
var applies = child is T t && (condition == null || condition(t));
@ -450,10 +717,16 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc cref="GetChildren{T}"/>
public IEnumerable<Element> GetChildren(Func<Element, bool> condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) {
return this.GetChildren<Element>(condition, regardGrandchildren, ignoreFalseGrandchildren);
}
/// <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>
public IEnumerable<Element> GetParentTree() {
if (this.Parent == null)
yield break;
@ -462,10 +735,19 @@ namespace MLEM.Ui.Elements {
yield return parent;
}
/// <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>
protected virtual List<Element> GetRelevantChildren() {
return this.SortedChildren;
}
/// <summary>
/// Updates this element and all of its <see cref="GetRelevantChildren"/>
/// </summary>
/// <param name="time">The game's time</param>
public virtual void Update(GameTime time) {
this.System.OnElementUpdated?.Invoke(this, time);
@ -474,6 +756,16 @@ namespace MLEM.Ui.Elements {
child.Update(time);
}
/// <summary>
/// Draws this element and all of its <see cref="GetRelevantChildren"/>
/// Note that, when this is called, <see cref="SpriteBatch.Begin"/> has already 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>
/// <param name="matrix">The transformation matrix that is used for drawing</param>
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
this.System.OnElementDrawn?.Invoke(this, time, batch, alpha);
if (this.Controls.SelectedElement == this)
@ -485,6 +777,17 @@ namespace MLEM.Ui.Elements {
}
}
/// <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>
/// <param name="matrix">The transformation matrix that is used for drawing</param>
public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
foreach (var child in this.GetRelevantChildren()) {
if (!child.IsHidden)
@ -492,6 +795,11 @@ namespace MLEM.Ui.Elements {
}
}
/// <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>
public virtual Element GetElementUnderPos(Vector2 position) {
if (this.IsHidden)
return null;
@ -504,38 +812,92 @@ namespace MLEM.Ui.Elements {
return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null;
}
/// <summary>
/// Performs the specified action on this element and all of its <see cref="Children"/>
/// </summary>
/// <param name="action">The action to perform</param>
public void AndChildren(Action<Element> action) {
action(this);
foreach (var child in this.Children)
child.AndChildren(action);
}
/// <summary>
/// Sorts this element's <see cref="Children"/> using the given comparison.
/// </summary>
/// <param name="comparison">The comparison to sort by</param>
public void ReorderChildren(Comparison<Element> comparison) {
this.Children.Sort(comparison);
}
/// <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>
public void ReverseChildren(int index = 0, int? count = null) {
this.Children.Reverse(index, count ?? this.Children.Count);
}
/// <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>
protected virtual void InitStyle(UiStyle style) {
this.SelectionIndicator.SetFromStyle(style.SelectionIndicator);
this.ActionSound.SetFromStyle(style.ActionSound?.CreateInstance());
this.SecondActionSound.SetFromStyle(style.ActionSound?.CreateInstance());
}
/// <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>
public delegate void TextInputCallback(Element element, Keys key, char character);
/// <summary>
/// A generic element-specific delegate.
/// </summary>
/// <param name="element">The current element</param>
public delegate void GenericCallback(Element element);
/// <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>
public delegate void OtherElementCallback(Element thisElement, Element otherElement);
/// <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>
public delegate void DrawCallback(Element element, GameTime time, SpriteBatch batch, float alpha);
/// <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>
public delegate void TimeCallback(Element element, GameTime time);
/// <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>
public delegate Element TabNextElementCallback(bool backward, Element usualNext);
/// <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>
public delegate Element GamepadNextElementCallback(Direction2 dir, Element usualNext);
}

View file

@ -3,8 +3,22 @@ using MLEM.Input;
using MLEM.Textures;
namespace MLEM.Ui.Elements {
/// <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>
public static class ElementHelper {
/// <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="tooltipWidth">The width 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>
public static Button ImageButton(Anchor anchor, Vector2 size, TextureRegion texture, string text = null, string tooltipText = null, float tooltipWidth = 50, float imagePadding = 2) {
var button = new Button(anchor, size, text, tooltipText, tooltipWidth);
var image = new Image(Anchor.CenterLeft, Vector2.One, texture) {Padding = new Vector2(imagePadding)};
@ -13,6 +27,17 @@ namespace MLEM.Ui.Elements {
return button;
}
/// <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>
public static Panel ShowInfoBox(UiSystem system, Anchor anchor, float width, string text, float buttonHeight = 10, string okText = "Okay") {
var group = new Group(Anchor.TopLeft, Vector2.One, false);
var box = group.AddChild(new Panel(anchor, new Vector2(width, 1), Vector2.Zero, true));
@ -26,6 +51,14 @@ namespace MLEM.Ui.Elements {
return box;
}
/// <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>
public static Group[] MakeColumns(Element parent, Vector2 totalSize, int amount, bool setHeightBasedOnChildren = true) {
var cols = new Group[amount];
for (var i = 0; i < amount; i++) {
@ -37,6 +70,16 @@ namespace MLEM.Ui.Elements {
return cols;
}
/// <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>
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);

View file

@ -2,13 +2,24 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MLEM.Ui.Elements {
/// <summary>
/// A group element to be used inside of a <see cref="UiSystem"/>.
/// A group is an element that has no rendering or interaction on its own, but that can aid with automatic placement of child elements.
/// </summary>
public class Group : Element {
/// <summary>
/// Creates a new group with the given settings
/// </summary>
/// <param name="anchor">The group's anchor</param>
/// <param name="size">The group's size</param>
/// <param name="setHeightBasedOnChildren">Whether the group's height should be based on its children's height</param>
public Group(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = true) : base(anchor, size) {
this.SetHeightBasedOnChildren = setHeightBasedOnChildren;
this.CanBeSelected = false;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
// since the group never accesses its own area when drawing, we have to update it manually
this.UpdateAreaIfDirty();

View file

@ -6,11 +6,25 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// An image element to be used inside of a <see cref="UiSystem"/>.
/// An image is simply an element that displays a supplied <see cref="TextureRegion"/> and optionally allows for the texture region to remain at its original aspect ratio, regardless of the element's size.
/// </summary>
public class Image : Element {
/// <summary>
/// The color to render the image at
/// </summary>
public StyleProp<Color> Color;
private TextureRegion texture;
/// <summary>
/// A callback to retrieve the <see cref="TextureRegion"/> that this image should render.
/// This can be used if the image changes frequently.
/// </summary>
public TextureCallback GetTextureCallback;
/// <summary>
/// The texture that this <see cref="TextureRegion"/> should render
/// </summary>
public TextureRegion Texture {
get {
if (this.GetTextureCallback != null)
@ -27,6 +41,9 @@ namespace MLEM.Ui.Elements {
}
}
private bool scaleToImage;
/// <summary>
/// Whether this image element's <see cref="Element.Size"/> should be based on the size of the <see cref="TextureRegion"/> given.
/// </summary>
public bool ScaleToImage {
get => this.scaleToImage;
set {
@ -36,11 +53,32 @@ namespace MLEM.Ui.Elements {
}
}
}
/// <summary>
/// Whether to cause the <see cref="TextureRegion"/> to be rendered at its proper aspect ratio.
/// If this is false, the image will be stretched according to this component's size.
/// </summary>
public bool MaintainImageAspect = true;
/// <summary>
/// The <see cref="SpriteEffects"/> that the texture should be rendered with
/// </summary>
public SpriteEffects ImageEffects = SpriteEffects.None;
/// <summary>
/// The scale that the image should be rendered with
/// </summary>
public Vector2 ImageScale = Vector2.One;
/// <summary>
/// The rotation that the image should be rendered with.
/// Note that increased rotation does not increase this component's size, even if the rotated texture would go out of bounds of this component.
/// </summary>
public float ImageRotation;
/// <summary>
/// Creates a new image with the given settings
/// </summary>
/// <param name="anchor">The image's anchor</param>
/// <param name="size">The image's size</param>
/// <param name="texture">The texture the image should render</param>
/// <param name="scaleToImage">Whether this image's size should be based on the texture's size</param>
public Image(Anchor anchor, Vector2 size, TextureRegion texture, bool scaleToImage = false) : base(anchor, size) {
this.Texture = texture;
this.scaleToImage = scaleToImage;
@ -48,6 +86,7 @@ namespace MLEM.Ui.Elements {
this.CanBeMoused = false;
}
/// <inheritdoc cref="Image(Anchor,Vector2,TextureRegion,bool)"/>
public Image(Anchor anchor, Vector2 size, TextureCallback getTextureCallback, bool scaleToImage = false) : base(anchor, size) {
this.GetTextureCallback = getTextureCallback;
this.Texture = getTextureCallback(this);
@ -56,10 +95,12 @@ namespace MLEM.Ui.Elements {
this.CanBeMoused = false;
}
/// <inheritdoc />
protected override Vector2 CalcActualSize(RectangleF parentArea) {
return this.Texture != null && this.scaleToImage ? this.Texture.Size.ToVector2() : base.CalcActualSize(parentArea);
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
if (this.Texture == null)
return;
@ -76,6 +117,10 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <summary>
/// A delegate method used for <see cref="Image.GetTextureCallback"/>
/// </summary>
/// <param name="image">The current image element</param>
public delegate TextureRegion TextureCallback(Image image);
}

View file

@ -10,15 +10,38 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A panel element to be used inside of a <see cref="UiSystem"/>.
/// The panel is a complex element that displays a box as a background to all of its child elements.
/// Additionally, a panel can be set to <see cref="scrollOverflow"/> on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a <see cref="ScrollBar"/>.
/// As this behavior is accomplished using a <see cref="RenderTarget2D"/>, scrolling panels need to have their <see cref="DrawEarly"/> methods called using <see cref="UiSystem.DrawEarly"/>.
/// </summary>
public class Panel : Element {
/// <summary>
/// The texture that this panel should have, or null if it should be invisible.
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// The scroll bar that this panel contains.
/// This is only nonnull if <see cref="scrollOverflow"/> is true.
/// </summary>
public readonly ScrollBar ScrollBar;
private readonly bool scrollOverflow;
private RenderTarget2D renderTarget;
private readonly List<Element> relevantChildren = new List<Element>();
private bool relevantChildrenDirty;
/// <summary>
/// Creates a new panel with the given settings.
/// </summary>
/// <param name="anchor">The panel's anchor</param>
/// <param name="size">The panel's default size</param>
/// <param name="positionOffset">The panel's offset from its anchor point</param>
/// <param name="setHeightBasedOnChildren">Whether the panel should automatically calculate its height based on its children's size</param>
/// <param name="scrollOverflow">Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers</param>
/// <param name="scrollerSize">The size of the <see cref="ScrollBar"/>'s scroller</param>
/// <param name="autoHideScrollbar">Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling</param>
public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, Point? scrollerSize = null, bool autoHideScrollbar = true) : base(anchor, size) {
this.PositionOffset = positionOffset;
this.SetHeightBasedOnChildren = setHeightBasedOnChildren;
@ -53,6 +76,7 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override void ForceUpdateArea() {
if (this.scrollOverflow) {
// sanity check
@ -67,7 +91,7 @@ namespace MLEM.Ui.Elements {
}
base.ForceUpdateArea();
this.ScrollChildren();
this.ScrollSetup();
}
@ -81,12 +105,14 @@ namespace MLEM.Ui.Elements {
this.relevantChildrenDirty = true;
}
/// <inheritdoc />
public override void ForceUpdateSortedChildren() {
base.ForceUpdateSortedChildren();
if (this.scrollOverflow)
this.relevantChildrenDirty = true;
}
/// <inheritdoc />
protected override List<Element> GetRelevantChildren() {
var relevant = base.GetRelevantChildren();
if (this.scrollOverflow) {
@ -113,6 +139,7 @@ namespace MLEM.Ui.Elements {
return relevant;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
if (this.Texture.HasValue())
batch.Draw(this.Texture, this.DisplayArea, Color.White * alpha, this.Scale);
@ -125,6 +152,7 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
this.UpdateAreaIfDirty();
if (this.scrollOverflow && this.renderTarget != null) {
@ -146,6 +174,7 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override Element GetElementUnderPos(Vector2 position) {
// if overflow is handled, don't propagate mouse checks to hidden children
if (this.scrollOverflow && !this.GetRenderTargetArea().Contains(position))
@ -160,11 +189,15 @@ namespace MLEM.Ui.Elements {
return area;
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.PanelTexture);
}
/// <summary>
/// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value.
/// </summary>
protected virtual void ScrollSetup() {
if (!this.scrollOverflow)
return;

View file

@ -15,12 +15,21 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A paragraph element for use inside of a <see cref="UiSystem"/>.
/// A paragraph is an element that contains text.
/// A paragraph's text can be formatted using the ui system's <see cref="UiSystem.TextFormatter"/>.
/// </summary>
public class Paragraph : Element {
private string text;
private string splitText;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public FormattingCodeCollection Formatting;
/// <summary>
/// The font that this paragraph draws text with.
/// To set its bold and italic font, use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
/// </summary>
public StyleProp<GenericFont> RegularFont;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
public StyleProp<GenericFont> BoldFont;
@ -28,10 +37,23 @@ namespace MLEM.Ui.Elements {
public StyleProp<GenericFont> ItalicFont;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public StyleProp<FormatSettings> FormatSettings;
/// <summary>
/// The tokenized version of the <see cref="Text"/>
/// </summary>
public TokenizedString TokenizedText { get; private set; }
/// <summary>
/// The color that the text will be rendered with
/// </summary>
public StyleProp<Color> TextColor;
/// <summary>
/// The scale that the text will be rendered with
/// </summary>
public StyleProp<float> TextScale;
/// <summary>
/// The text to render inside of this paragraph.
/// Use <see cref="GetTextCallback"/> if the text changes frequently.
/// </summary>
public string Text {
get {
this.QueryTextCallback();
@ -47,13 +69,27 @@ namespace MLEM.Ui.Elements {
}
}
}
/// <summary>
/// If this paragraph should automatically adjust its width based on the width of the text within it
/// </summary>
public bool AutoAdjustWidth;
/// <summary>
/// An event that gets called when this paragraph's <see cref="Text"/> is queried.
/// Use this event for setting this paragraph's text if it changes frequently.
/// </summary>
public TextCallback GetTextCallback;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public TextModifier RenderedTextModifier = text => text;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public TimeSpan TimeIntoAnimation;
/// <summary>
/// Creates a new paragraph with the given settings.
/// </summary>
/// <param name="anchor">The paragraph's anchor</param>
/// <param name="width">The paragraph's width. Note that its height is automatically calculated.</param>
/// <param name="textCallback">The paragraph's text</param>
/// <param name="centerText">Whether the paragraph's width should automatically be calculated based on the text within it.</param>
public Paragraph(Anchor anchor, float width, TextCallback textCallback, bool centerText = false)
: this(anchor, width, "", centerText) {
this.GetTextCallback = textCallback;
@ -62,6 +98,7 @@ namespace MLEM.Ui.Elements {
this.IsHidden = true;
}
/// <inheritdoc cref="Paragraph(Anchor,float,TextCallback,bool)"/>
public Paragraph(Anchor anchor, float width, string text, bool centerText = false) : base(anchor, new Vector2(width, 0)) {
this.Text = text;
if (this.Text == null)
@ -71,6 +108,7 @@ namespace MLEM.Ui.Elements {
this.CanBeMoused = false;
}
/// <inheritdoc />
protected override Vector2 CalcActualSize(RectangleF parentArea) {
var size = base.CalcActualSize(parentArea);
this.ParseText(size);
@ -85,6 +123,7 @@ namespace MLEM.Ui.Elements {
return new Vector2(this.AutoAdjustWidth ? dims.X + this.ScaledPadding.Width : size.X, dims.Y + this.ScaledPadding.Height);
}
/// <inheritdoc />
public override void Update(GameTime time) {
this.QueryTextCallback();
base.Update(time);
@ -95,6 +134,7 @@ namespace MLEM.Ui.Elements {
this.TokenizedText.Update(time);
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var pos = this.DisplayArea.Location;
var sc = this.TextScale * this.Scale;
@ -110,6 +150,7 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.TextScale.SetFromStyle(style.TextScale);
@ -119,6 +160,11 @@ namespace MLEM.Ui.Elements {
this.FormatSettings.SetFromStyle(style.FormatSettings);
}
/// <summary>
/// Parses this paragraph's <see cref="Text"/> into <see cref="TokenizedText"/>.
/// Additionally, this method adds any <see cref="Link"/> elements for tokenized links in the text.
/// </summary>
/// <param name="size">The paragraph's default size</param>
protected virtual void ParseText(Vector2 size) {
// old formatting stuff
this.splitText = this.RegularFont.Value.SplitString(this.Text.RemoveFormatting(this.RegularFont.Value), size.X - this.ScaledPadding.Width, this.TextScale * this.Scale);
@ -152,16 +198,37 @@ namespace MLEM.Ui.Elements {
this.Text = this.GetTextCallback(this);
}
/// <summary>
/// A delegate method used for <see cref="Paragraph.GetTextCallback"/>
/// </summary>
/// <param name="paragraph">The current paragraph</param>
public delegate string TextCallback(Paragraph paragraph);
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public delegate string TextModifier(string text);
/// <summary>
/// A link is a sub-element of the <see cref="Paragraph"/> that is added onto it as a child for any tokens that contain <see cref="LinkCode"/>, to make them selectable and clickable.
/// </summary>
public class Link : Element {
/// <summary>
/// The token that this link represents
/// </summary>
public readonly Token Token;
/// <summary>
/// The links that form a cluster for the given token.
/// This only contains more than one element if the tokenized string has previously been <see cref="TokenizedString.Split"/>.
/// </summary>
public readonly Link[] LinkCluster;
/// <summary>
/// Creates a new link element with the given settings
/// </summary>
/// <param name="anchor">The link's anchor</param>
/// <param name="token">The token that this link represents</param>
/// <param name="size">The size of the token</param>
/// <param name="linkCluster">The links that form a cluster for the given token. This only contains more than one element if the tokenized string has previously been split.</param>
public Link(Anchor anchor, Token token, Vector2 size, Link[] linkCluster) : base(anchor, size) {
this.Token = token;
this.LinkCluster = linkCluster;
@ -176,6 +243,7 @@ namespace MLEM.Ui.Elements {
};
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
if (this.LinkCluster.Length > 1 && this.Controls.SelectedElement == this) {
// also draw the selection box around all other links in the cluster

View file

@ -7,22 +7,62 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A progress bar element to use inside of a <see cref="UiSystem"/>.
/// A progress bar is an element that fills up a bar based on a given <see cref="currentValue"/> percentage.
/// </summary>
public class ProgressBar : Element {
/// <summary>
/// The background texture that this progress bar should render
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// The color that this progress bar's <see cref="Texture"/> should render with
/// </summary>
public StyleProp<Color> Color;
/// <summary>
/// The padding that this progress bar's <see cref="ProgressTexture"/> should have.
/// The padding is the amount of pixels that the progress texture is away from the borders of the progress bar.
/// </summary>
public StyleProp<Vector2> ProgressPadding;
/// <summary>
/// The texture that this progress bar's progress should render
/// </summary>
public StyleProp<NinePatch> ProgressTexture;
/// <summary>
/// The color that this progress bar's <see cref="ProgressTexture"/> is rendered with.
/// </summary>
public StyleProp<Color> ProgressColor;
/// <summary>
/// The direction that this progress bar goes in.
/// Note that only <see cref="Direction2Helper.Adjacent"/> directions are supported.
/// </summary>
public Direction2 Direction;
/// <summary>
/// The maximum value that this progress bar should be able to have.
/// </summary>
public float MaxValue;
private float currentValue;
/// <summary>
/// The current value that this progress bar has.
/// This value is always between 0 and <see cref="MaxValue"/>.
/// </summary>
public float CurrentValue {
get => this.currentValue;
set => this.currentValue = MathHelper.Clamp(value, 0, this.MaxValue);
}
/// <summary>
/// Creates a new progress bar with the given settings
/// </summary>
/// <param name="anchor">The progress bar's anchor</param>
/// <param name="size">The size of the progress bar</param>
/// <param name="direction">The direction that the progress bar goes into</param>
/// <param name="maxValue">The progress bar's maximum value</param>
/// <param name="currentValue">The progress bar's current value</param>
/// <exception cref="NotSupportedException">If the provided direction is not <see cref="Direction2Helper.IsAdjacent"/></exception>
public ProgressBar(Anchor anchor, Vector2 size, Direction2 direction, float maxValue, float currentValue = 0) : base(anchor, size) {
if (!direction.IsAdjacent())
throw new NotSupportedException("Progress bars only support Up, Down, Left and Right directions");
@ -32,6 +72,7 @@ namespace MLEM.Ui.Elements {
this.CanBeSelected = false;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
batch.Draw(this.Texture, this.DisplayArea, (Color) this.Color * alpha, this.Scale);
@ -68,6 +109,7 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.ProgressBarTexture);

View file

@ -3,10 +3,26 @@ using MLEM.Input;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A radio button element to use inside of a <see cref="UiSystem"/>.
/// A radio button is a variation of a <see cref="Checkbox"/> that causes all other radio buttons in the same <see cref="Group"/> to be deselected upon selection.
/// </summary>
public class RadioButton : Checkbox {
/// <summary>
/// The group that this radio button has.
/// All other radio buttons in the same <see cref="RootElement"/> that have the same group will be deselected when this radio button is selected.
/// </summary>
public string Group;
/// <summary>
/// Creates a new radio button with the given settings
/// </summary>
/// <param name="anchor">The radio button's anchor</param>
/// <param name="size">The radio button's size</param>
/// <param name="label">The label to display next to the radio button</param>
/// <param name="defaultChecked">If the radio button should be checked by default</param>
/// <param name="group">The group that the radio button has</param>
public RadioButton(Anchor anchor, Vector2 size, string label, bool defaultChecked = false, string group = "") :
base(anchor, size, label, defaultChecked) {
this.Group = group;
@ -21,6 +37,7 @@ namespace MLEM.Ui.Elements {
};
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.RadioTexture);

View file

@ -10,13 +10,34 @@ using MLEM.Textures;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A scroll bar element to be used inside of a <see cref="UiSystem"/>.
/// A scroll bar is an element that features a smaller scroller indicator inside of it that can move up and down.
/// A scroll bar can be scrolled using the mouse or by using the scroll wheel while hovering over its <see cref="Element.Parent"/> or any of its siblings.
/// </summary>
public class ScrollBar : Element {
/// <summary>
/// The background texture for this scroll bar
/// </summary>
public StyleProp<NinePatch> Background;
/// <summary>
/// The texture of this scroll bar's scroller indicator
/// </summary>
public StyleProp<NinePatch> ScrollerTexture;
/// <summary>
/// The <see cref="ScrollerTexture"/>'s offset from the calculated position of the scroller. Use this to pad the scroller.
/// </summary>
public Vector2 ScrollerOffset;
/// <summary>
/// The scroller's width and height
/// </summary>
public Vector2 ScrollerSize;
private float maxValue;
/// <summary>
/// The max value that this scroll bar should be able to scroll to.
/// Note that the maximum value does not change the height of the scroll bar.
/// </summary>
public float MaxValue {
get => this.maxValue;
set {
@ -31,6 +52,10 @@ namespace MLEM.Ui.Elements {
}
private float scrollAdded;
private float currValue;
/// <summary>
/// The current value of the scroll bar.
/// This is between 0 and <see cref="MaxValue"/> at all times.
/// </summary>
public float CurrentValue {
get => this.currValue - this.scrollAdded;
set {
@ -43,21 +68,51 @@ namespace MLEM.Ui.Elements {
}
}
}
/// <summary>
/// Whether this scroll bar is horizontal
/// </summary>
public readonly bool Horizontal;
/// <summary>
/// The amount added or removed from <see cref="CurrentValue"/> per single movement of the scroll wheel
/// </summary>
public float StepPerScroll = 1;
/// <summary>
/// An event that is called when <see cref="CurrentValue"/> changes
/// </summary>
public ValueChanged OnValueChanged;
/// <summary>
/// An event that is called when this scroll bar is automatically hidden from a <see cref="Panel"/>
/// </summary>
public GenericCallback OnAutoHide;
private bool isMouseHeld;
private bool isDragging;
private bool isTouchHeld;
/// <summary>
/// This field determines if this scroll bar should automatically be hidden from a <see cref="Panel"/> if there aren't enough children to allow for scrolling.
/// </summary>
public bool AutoHideWhenEmpty;
/// <summary>
/// Whether smooth scrolling should be enabled for this scroll bar.
/// Smooth scrolling causes the <see cref="CurrentValue"/> to change gradually rather than instantly when scrolling.
/// </summary>
public bool SmoothScrolling;
/// <summary>
/// The factor with which <see cref="SmoothScrolling"/> happens.
/// </summary>
public float SmoothScrollFactor = 0.75F;
static ScrollBar() {
InputHandler.EnableGestures(GestureType.HorizontalDrag, GestureType.VerticalDrag);
}
/// <summary>
/// Creates a new scroll bar with the given settings
/// </summary>
/// <param name="anchor">The scroll bar's anchor</param>
/// <param name="size">The scroll bar's size</param>
/// <param name="scrollerSize"></param>
/// <param name="maxValue"></param>
/// <param name="horizontal"></param>
public ScrollBar(Anchor anchor, Vector2 size, int scrollerSize, float maxValue, bool horizontal = false) : base(anchor, size) {
this.maxValue = maxValue;
this.Horizontal = horizontal;
@ -65,6 +120,7 @@ namespace MLEM.Ui.Elements {
this.CanBeSelected = false;
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
@ -134,6 +190,7 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
batch.Draw(this.Background, this.DisplayArea, Color.White * alpha, this.Scale);
@ -148,12 +205,18 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Background.SetFromStyle(style.ScrollBarBackground);
this.ScrollerTexture.SetFromStyle(style.ScrollBarScrollerTexture);
}
/// <summary>
/// A delegate method used for <see cref="ScrollBar.OnValueChanged"/>
/// </summary>
/// <param name="element">The element whose current value changed</param>
/// <param name="value">The element's new <see cref="ScrollBar.CurrentValue"/></param>
public delegate void ValueChanged(Element element, float value);
}

View file

@ -4,8 +4,19 @@ using Microsoft.Xna.Framework.Input;
using MLEM.Misc;
namespace MLEM.Ui.Elements {
/// <summary>
/// A slider element for use inside of a <see cref="UiSystem"/>.
/// A slider is a horizontal <see cref="ScrollBar"/> whose value can additionally be controlled using the <see cref="UiControls.LeftButtons"/> and <see cref="UiControls.RightButtons"/>.
/// </summary>
public class Slider : ScrollBar {
/// <summary>
/// Creates a new slider with the given settings
/// </summary>
/// <param name="anchor">The slider's anchor</param>
/// <param name="size">The slider's size</param>
/// <param name="scrollerSize">The size of the slider's scroller indicator</param>
/// <param name="maxValue">The slider's maximum value</param>
public Slider(Anchor anchor, Vector2 size, int scrollerSize, float maxValue) :
base(anchor, size, scrollerSize, maxValue, true) {
this.CanBeSelected = true;
@ -16,6 +27,7 @@ namespace MLEM.Ui.Elements {
};
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);

View file

@ -3,19 +3,41 @@ using MLEM.Animations;
using MLEM.Textures;
namespace MLEM.Ui.Elements {
/// <summary>
/// A sprite animation image for use inside of a <see cref="UiSystem"/>.
/// A sprite animation image is an <see cref="Image"/> that displays a <see cref="SpriteAnimation"/> or <see cref="SpriteAnimationGroup"/>.
/// </summary>
public class SpriteAnimationImage : Image {
/// <summary>
/// The sprite animation group that is displayed by this image
/// </summary>
public SpriteAnimationGroup Group;
public SpriteAnimationImage(Anchor anchor, Vector2 size, TextureRegion texture, SpriteAnimationGroup group, bool scaleToImage = false) :
base(anchor, size, texture, scaleToImage) {
/// <summary>
/// Creates a new sprite animation image with the given settings
/// </summary>
/// <param name="anchor">The image's anchor</param>
/// <param name="size">The image's size</param>
/// <param name="group">The sprite animation group to display</param>
/// <param name="scaleToImage">Whether this image element should scale to the texture</param>
public SpriteAnimationImage(Anchor anchor, Vector2 size, SpriteAnimationGroup group, bool scaleToImage = false) :
base(anchor, size, group.CurrentRegion, scaleToImage) {
this.Group = group;
}
public SpriteAnimationImage(Anchor anchor, Vector2 size, TextureRegion texture, SpriteAnimation animation, bool scaleToImage = false) :
this(anchor, size, texture, new SpriteAnimationGroup().Add(animation, () => true), scaleToImage) {
/// <summary>
/// Creates a new sprite animation image with the given settings
/// </summary>
/// <param name="anchor">The image's anchor</param>
/// <param name="size">The image's size</param>
/// <param name="animation">The sprite group to display</param>
/// <param name="scaleToImage">Whether this image element should scale to the texture</param>
public SpriteAnimationImage(Anchor anchor, Vector2 size, SpriteAnimation animation, bool scaleToImage = false) :
this(anchor, size, new SpriteAnimationGroup().Add(animation, () => true), scaleToImage) {
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
this.Group.Update(time);
@ -23,5 +45,4 @@ namespace MLEM.Ui.Elements {
}
}
}

View file

@ -13,33 +13,100 @@ using MLEM.Ui.Style;
using TextCopy;
namespace MLEM.Ui.Elements {
/// <summary>
/// A text field element for use inside of a <see cref="UiSystem"/>.
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
/// If <see cref="TextInputWrapper.RequiresOnScreenKeyboard"/> is enabled, then this text field will automatically open an on-screen keyboard when pressed using <see cref="KeyboardInput"/>.
/// </summary>
public class TextField : Element {
/// <summary>
/// A <see cref="Rule"/> that allows any visible character and spaces
/// </summary>
public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl);
/// <summary>
/// A <see cref="Rule"/> that only allows letters
/// </summary>
public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter);
/// <summary>
/// A <see cref="Rule"/> that only allows numerals
/// </summary>
public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber);
/// <summary>
/// A <see cref="Rule"/> that only allows letters and numerals
/// </summary>
public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c));
/// <summary>
/// The color that this text field's text should display with
/// </summary>
public StyleProp<Color> TextColor;
/// <summary>
/// The color that the <see cref="PlaceholderText"/> should display with
/// </summary>
public StyleProp<Color> PlaceholderColor;
/// <summary>
/// This text field's texture
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// This text field's texture while it is hovered
/// </summary>
public StyleProp<NinePatch> HoveredTexture;
/// <summary>
/// The color that this text field should display with while it is hovered
/// </summary>
public StyleProp<Color> HoveredColor;
/// <summary>
/// The scale that this text field should render text with
/// </summary>
public StyleProp<float> TextScale;
/// <summary>
/// The font that this text field should display text with
/// </summary>
public StyleProp<GenericFont> Font;
private readonly StringBuilder text = new StringBuilder();
/// <summary>
/// This text field's current text
/// </summary>
public string Text => this.text.ToString();
/// <summary>
/// The text that displays in this text field if <see cref="Text"/> is empty
/// </summary>
public string PlaceholderText;
/// <summary>
/// An event that gets called when <see cref="Text"/> changes, either through input, or through a manual change.
/// </summary>
public TextChanged OnTextChange;
/// <summary>
/// The x position that text should start rendering at, based on the x position of this text field.
/// </summary>
public float TextOffsetX = 4;
/// <summary>
/// The width that the caret should render with.
/// </summary>
public float CaretWidth = 0.5F;
private double caretBlinkTimer;
private string displayedText;
private int textOffset;
/// <summary>
/// The rule used for text input.
/// Rules allow only certain characters to be allowed inside of a text field.
/// </summary>
public Rule InputRule;
/// <summary>
/// The title of the <see cref="KeyboardInput"/> field on mobile devices and consoles
/// </summary>
public string MobileTitle;
/// <summary>
/// The description of the <see cref="KeyboardInput"/> field on mobile devices and consoles
/// </summary>
public string MobileDescription;
private int caretPos;
/// <summary>
/// The position of the caret within the text.
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
/// </summary>
public int CaretPos {
get {
this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
@ -53,6 +120,13 @@ namespace MLEM.Ui.Elements {
}
}
/// <summary>
/// Creates a new text field with the given settings
/// </summary>
/// <param name="anchor">The text field's anchor</param>
/// <param name="size">The text field's size</param>
/// <param name="rule">The text field's input rule</param>
/// <param name="font">The font to use for drawing text</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null) : base(anchor, size) {
this.InputRule = rule ?? DefaultRule;
if (font != null)
@ -115,6 +189,7 @@ namespace MLEM.Ui.Elements {
this.OnTextChange?.Invoke(this, this.Text);
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
@ -149,6 +224,7 @@ namespace MLEM.Ui.Elements {
this.caretBlinkTimer = 0;
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
var tex = this.Texture;
var color = Color.White * alpha;
@ -175,6 +251,11 @@ namespace MLEM.Ui.Elements {
base.Draw(time, batch, alpha, blendState, samplerState, matrix);
}
/// <summary>
/// Replaces this text field's text with the given text.
/// </summary>
/// <param name="text">The new text</param>
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
public void SetText(object text, bool removeMismatching = false) {
if (removeMismatching) {
var result = new StringBuilder();
@ -191,6 +272,10 @@ namespace MLEM.Ui.Elements {
this.HandleTextChange();
}
/// <summary>
/// Inserts the given text at the <see cref="CaretPos"/>
/// </summary>
/// <param name="text">The text to insert</param>
public void InsertText(object text) {
var strg = text.ToString();
if (!this.InputRule(this, strg))
@ -200,6 +285,11 @@ namespace MLEM.Ui.Elements {
this.HandleTextChange();
}
/// <summary>
/// Removes the given amount of text at the given index
/// </summary>
/// <param name="index">The index</param>
/// <param name="length">The amount of text to remove</param>
public void RemoveText(int index, int length) {
if (index < 0 || index >= this.text.Length)
return;
@ -207,6 +297,7 @@ namespace MLEM.Ui.Elements {
this.HandleTextChange();
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.TextScale.SetFromStyle(style.TextScale);
@ -216,8 +307,19 @@ namespace MLEM.Ui.Elements {
this.HoveredColor.SetFromStyle(style.TextFieldHoveredColor);
}
/// <summary>
/// A delegate method used for <see cref="TextField.OnTextChange"/>
/// </summary>
/// <param name="field">The text field whose text changed</param>
/// <param name="text">The new text</param>
public delegate void TextChanged(TextField field, string text);
/// <summary>
/// A delegate method used for <see cref="InputRule"/>.
/// It should return whether the given text can be added to the text field.
/// </summary>
/// <param name="field">The text field</param>
/// <param name="textToAdd">The text that is tried to be added</param>
public delegate bool Rule(TextField field, string textToAdd);
}

View file

@ -5,11 +5,28 @@ using MLEM.Font;
using MLEM.Ui.Style;
namespace MLEM.Ui.Elements {
/// <summary>
/// A tooltip element for use inside of a <see cref="UiSystem"/>.
/// A tooltip is a <see cref="Panel"/> with a custom cursor that always follows the position of the mouse.
/// Tooltips can easily be configured to be hooked onto an element, causing them to appear when it is moused, and disappear when it is not moused anymore.
/// </summary>
public class Tooltip : Panel {
/// <summary>
/// The offset that this tooltip's top left corner should have from the mouse position
/// </summary>
public StyleProp<Vector2> MouseOffset;
/// <summary>
/// The paragraph of text that this tooltip displays
/// </summary>
public Paragraph Paragraph;
/// <summary>
/// Creates a new tooltip with the given settings
/// </summary>
/// <param name="width">The width of the tooltip</param>
/// <param name="text">The text to display on the tooltip</param>
/// <param name="elementToHover">The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively</param>
public Tooltip(float width, string text = null, Element elementToHover = null) :
base(Anchor.TopLeft, Vector2.One, Vector2.Zero) {
if (text != null) {
@ -33,23 +50,29 @@ namespace MLEM.Ui.Elements {
}
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
this.SnapPositionToMouse();
}
/// <inheritdoc />
public override void ForceUpdateArea() {
if (this.Parent != null)
throw new NotSupportedException($"A tooltip shouldn't be the child of another element ({this.Parent})");
base.ForceUpdateArea();
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.Texture.SetFromStyle(style.TooltipBackground);
this.MouseOffset.SetFromStyle(style.TooltipOffset);
}
/// <summary>
/// Causes this tooltip's position to be snapped to the mouse position.
/// </summary>
public void SnapPositionToMouse() {
var viewport = this.System.Viewport.Size;
var offset = this.Input.MousePosition.ToVector2() / this.Scale + this.MouseOffset;

View file

@ -1,8 +1,16 @@
using Microsoft.Xna.Framework;
namespace MLEM.Ui.Elements {
/// <summary>
/// A vertical space element for use inside of a <see cref="UiSystem"/>.
/// A vertical space is an invisible element that can be used to add vertical space between paragraphs or other elements.
/// </summary>
public class VerticalSpace : Element {
/// <summary>
/// Creates a new vertical space with the given settings
/// </summary>
/// <param name="height">The height of the vertical space</param>
public VerticalSpace(int height) : base(Anchor.AutoCenter, new Vector2(1, height)) {
this.CanBeSelected = false;
}

View file

@ -1,39 +1,82 @@
using System.Collections.Generic;
using MLEM.Ui.Elements;
namespace MLEM.Ui.Style {
/// <summary>
/// A struct used by <see cref="Element"/> to store style properties.
/// This is a helper struct that allows default style settings from <see cref="UiStyle"/> to be overridden by custom user settings easily.
/// Note that <c>T</c> implicitly converts to <c>StyleProp{T}</c> and vice versa.
/// </summary>
/// <typeparam name="T">The type of style setting that this property stores</typeparam>
public struct StyleProp<T> {
/// <summary>
/// The currently applied style
/// </summary>
public T Value { get; private set; }
private bool isCustom;
/// <summary>
/// Creates a new style property with the given custom style.
/// </summary>
/// <param name="value">The custom style to apply</param>
public StyleProp(T value) {
this.isCustom = true;
this.Value = value;
}
/// <summary>
/// Sets this style property's value and marks it as being set by a <see cref="UiStyle"/>.
/// This allows this property to be overridden by custom style settings using <see cref="Set"/>.
/// </summary>
/// <param name="value">The style to apply</param>
public void SetFromStyle(T value) {
if (!this.isCustom) {
this.Value = value;
}
}
/// <summary>
/// Sets this style property's value and marks it as being custom.
/// This causes <see cref="SetFromStyle"/> not to override the style value through a <see cref="UiStyle"/>.
/// </summary>
/// <param name="value"></param>
public void Set(T value) {
this.isCustom = true;
this.Value = value;
}
/// <summary>
/// Returns the current style value or, if <see cref="HasValue"/> is false, the given default value.
/// </summary>
/// <param name="def">The default to return if this style property has no value</param>
/// <returns>The current value, or the default</returns>
public T OrDefault(T def) {
return this.HasValue() ? this.Value : def;
}
/// <summary>
/// Returns whether this style property has a value assigned to it using <see cref="SetFromStyle"/> or <see cref="Set"/>.
/// </summary>
/// <returns>Whether this style property has a value</returns>
public bool HasValue() {
return !EqualityComparer<T>.Default.Equals(this.Value, default);
}
/// <summary>
/// Implicitly converts a style property to its value.
/// </summary>
/// <param name="prop">The property to convert</param>
/// <returns>The style that the style property holds</returns>
public static implicit operator T(StyleProp<T> prop) {
return prop.Value;
}
/// <summary>
/// Implicitly converts a style to a style property.
/// </summary>
/// <param name="prop">The property to convert</param>
/// <returns>A style property with the given style value</returns>
public static implicit operator StyleProp<T>(T prop) {
return new StyleProp<T>(prop);
}

View file

@ -5,37 +5,129 @@ using MLEM.Font;
using MLEM.Formatting;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Elements;
namespace MLEM.Ui.Style {
/// <summary>
/// The style settings for a <see cref="UiSystem"/>.
/// Each <see cref="Element"/> uses these style settings by default, however you can also change these settings per element using the elements' individual style settings.
/// Note that this class is a <see cref="GenericDataHolder"/>, meaning additional styles for custom components can easily be added using <see cref="GenericDataHolder.SetData"/>
/// </summary>
public class UiStyle : GenericDataHolder {
/// <summary>
/// The texture that is rendered on top of the <see cref="UiControls.SelectedElement"/>
/// </summary>
public NinePatch SelectionIndicator;
/// <summary>
/// The texture that the <see cref="Button"/> element uses
/// </summary>
public NinePatch ButtonTexture;
/// <summary>
/// The texture that the <see cref="Button"/> element uses when it is moused over (<see cref="Element.IsMouseOver"/>)
/// Note that, if you just want to change the button's color when hovered, use <see cref="ButtonHoveredColor"/>.
/// </summary>
public NinePatch ButtonHoveredTexture;
/// <summary>
/// The color that the <see cref="Button"/> element renders with when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public Color ButtonHoveredColor;
/// <summary>
/// The texture that the <see cref="Button"/> element uses when it <see cref="Button.IsDisabled"/>
/// </summary>
public NinePatch ButtonDisabledTexture;
/// <summary>
/// The color that the <see cref="Button"/> element uses when it <see cref="Button.IsDisabled"/>
/// </summary>
public Color ButtonDisabledColor;
/// <summary>
/// The texture that the <see cref="Panel"/> element uses
/// </summary>
public NinePatch PanelTexture;
/// <summary>
/// The texture that the <see cref="TextField"/> element uses
/// </summary>
public NinePatch TextFieldTexture;
/// <summary>
/// The texture that the <see cref="TextField"/> element uses when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public NinePatch TextFieldHoveredTexture;
/// <summary>
/// The color that the <see cref="TextField"/> renders with when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public Color TextFieldHoveredColor;
/// <summary>
/// The background texture that the <see cref="ScrollBar"/> element uses
/// </summary>
public NinePatch ScrollBarBackground;
/// <summary>
/// The texture that the scroll indicator of the <see cref="ScrollBar"/> element uses
/// </summary>
public NinePatch ScrollBarScrollerTexture;
/// <summary>
/// The texture that the <see cref="Checkbox"/> element uses
/// </summary>
public NinePatch CheckboxTexture;
/// <summary>
/// The texture that the <see cref="Checkbox"/> element uses when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public NinePatch CheckboxHoveredTexture;
/// <summary>
/// The color that the <see cref="Checkbox"/> element renders with when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public Color CheckboxHoveredColor;
/// <summary>
/// The texture that the <see cref="Checkbox"/> element renders on top of its regular texture when it is <see cref="Checkbox.Checked"/>
/// </summary>
public TextureRegion CheckboxCheckmark;
/// <summary>
/// The texture that the <see cref="RadioButton"/> element uses
/// </summary>
public NinePatch RadioTexture;
/// <summary>
/// The texture that the <see cref="RadioButton"/> element uses when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public NinePatch RadioHoveredTexture;
/// <summary>
/// The color that the <see cref="RadioButton"/> element renders with when it is moused over (<see cref="Element.IsMouseOver"/>)
/// </summary>
public Color RadioHoveredColor;
/// <summary>
/// The texture that the <see cref="RadioButton"/> renders on top of its regular texture when it is <see cref="RadioButton.Checked"/>
/// </summary>
public TextureRegion RadioCheckmark;
/// <summary>
/// The texture that the <see cref="Tooltip"/> uses for its background
/// </summary>
public NinePatch TooltipBackground;
/// <summary>
/// The offset of the <see cref="Tooltip"/> element's top left corner from the mouse position
/// </summary>
public Vector2 TooltipOffset;
/// <summary>
/// The texture that the <see cref="ProgressBar"/> element uses for its background
/// </summary>
public NinePatch ProgressBarTexture;
/// <summary>
/// The color that the <see cref="ProgressBar"/> element renders with
/// </summary>
public Color ProgressBarColor;
/// <summary>
/// The padding that the <see cref="ProgressBar"/> uses for its progress texture (<see cref="ProgressBarProgressTexture"/>)
/// </summary>
public Vector2 ProgressBarProgressPadding;
/// <summary>
/// The texture that the <see cref="ProgressBar"/> uses for displaying its progress
/// </summary>
public NinePatch ProgressBarProgressTexture;
/// <summary>
/// The color that the <see cref="ProgressBar"/> renders its progress texture with
/// </summary>
public Color ProgressBarProgressColor;
/// <summary>
/// The font that <see cref="Paragraph"/> and other elements should use for rendering.
/// Note that, to specify a bold and italic font for <see cref="TextFormatter"/>, you should use <see cref="GenericFont.Bold"/> and <see cref="GenericFont.Italic"/>.
/// </summary>
public GenericFont Font;
[Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")]
public GenericFont BoldFont;
@ -43,7 +135,14 @@ namespace MLEM.Ui.Style {
public GenericFont ItalicFont;
[Obsolete("Use the new text formatting system in MLEM.Formatting instead")]
public FormatSettings FormatSettings;
/// <summary>
/// The scale that text should be rendered with in <see cref="Paragraph"/> and other elements
/// </summary>
public float TextScale = 1;
/// <summary>
/// The <see cref="SoundEffect"/> that should be played when an element's <see cref="Element.OnPressed"/> and <see cref="Element.OnSecondaryPressed"/> events are called.
/// Note that this sound is only played if the callbacks have any subscribers.
/// </summary>
public SoundEffect ActionSound;
}

View file

@ -7,8 +7,16 @@ using MLEM.Font;
using MLEM.Textures;
namespace MLEM.Ui.Style {
/// <summary>
/// The default, untextured <see cref="UiStyle"/>.
/// Note that, as MLEM does not provide any texture or font assets, this default style is made up of single-color textures that were generated using <see cref="SpriteBatchExtensions.GenerateTexture"/>.
/// </summary>
public class UntexturedStyle : UiStyle {
/// <summary>
/// Creates a new untextured style with textures generated by the given sprite batch
/// </summary>
/// <param name="batch">The sprite batch to generate the textures with</param>
public UntexturedStyle(SpriteBatch batch) {
this.SelectionIndicator = batch.GenerateTexture(Color.Transparent, Color.Red);
this.ButtonTexture = batch.GenerateTexture(Color.CadetBlue);

View file

@ -8,33 +8,119 @@ using MLEM.Extensions;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Ui.Elements;
using MLEM.Ui.Style;
namespace MLEM.Ui {
/// <summary>
/// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>.
/// UiControls supports keyboard, mouse, gamepad and touch input using an underlying <see cref="InputHandler"/>.
/// </summary>
public class UiControls {
/// <summary>
/// The input handler that is used for querying input
/// </summary>
public readonly InputHandler Input;
/// <summary>
/// This value ist true if the <see cref="InputHandler"/> was created by this ui controls instance, or if it was passed in.
/// If the input handler was created by this instance, its <see cref="InputHandler.Update()"/> method should be called by us.
/// </summary>
protected readonly bool IsInputOurs;
/// <summary>
/// The <see cref="UiSystem"/> that this ui controls instance is controlling
/// </summary>
protected readonly UiSystem System;
/// <summary>
/// The <see cref="RootElement"/> that is currently active.
/// The active root element is the one with the highest <see cref="RootElement.Priority"/> that whose <see cref="RootElement.CanSelectContent"/> property is true.
/// </summary>
public RootElement ActiveRoot { get; private set; }
/// <summary>
/// The <see cref="Element"/> that the mouse is currently over.
/// </summary>
public Element MousedElement { get; private set; }
private readonly Dictionary<string, Element> selectedElements = new Dictionary<string, Element>();
/// <summary>
/// The element that is currently selected.
/// This is the <see cref="RootElement.SelectedElement"/> of the <see cref="ActiveRoot"/>.
/// </summary>
public Element SelectedElement => this.GetSelectedElement(this.ActiveRoot);
/// <summary>
/// A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons on the keyboard which perform the <see cref="Element.OnPressed"/> action.
/// If the <see cref="ModifierKey.Shift"/> is held, these buttons perform <see cref="Element.OnSecondaryPressed"/>.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] KeyboardButtons = {Keys.Space, Keys.Enter};
/// <summary>
/// A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons on a gamepad that perform the <see cref="Element.OnPressed"/> action.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] GamepadButtons = {Buttons.A};
/// <summary>
/// A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons on a gamepad that perform the <see cref="Element.OnSecondaryPressed"/> action.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] SecondaryGamepadButtons = {Buttons.X};
/// <summary>
/// A list of A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons that select a <see cref="Element"/> that is above the currently selected element.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] UpButtons = {Buttons.DPadUp, Buttons.LeftThumbstickUp};
/// <summary>
/// A list of A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons that select a <see cref="Element"/> that is below the currently selected element.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] DownButtons = {Buttons.DPadDown, Buttons.LeftThumbstickDown};
/// <summary>
/// A list of A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons that select a <see cref="Element"/> that is to the left of the currently selected element.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] LeftButtons = {Buttons.DPadLeft, Buttons.LeftThumbstickLeft};
/// <summary>
/// A list of A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons that select a <see cref="Element"/> that is to the right of the currently selected element.
/// To easily add more elements to this list, use <see cref="AddButtons"/>.
/// </summary>
public object[] RightButtons = {Buttons.DPadRight, Buttons.LeftThumbstickRight};
/// <summary>
/// The zero-based index of the <see cref="GamePad"/> used for gamepad input.
/// If this index is lower than 0, every connected gamepad will trigger input.
/// </summary>
public int GamepadIndex = -1;
/// <summary>
/// Set this to false to disable mouse input for these ui controls.
/// Note that this does not disable mouse input for the underlying <see cref="InputHandler"/>.
/// </summary>
public bool HandleMouse = true;
/// <summary>
/// Set this to false to disable keyboard input for these ui controls.
/// Note that this does not disable keyboard input for the underlying <see cref="InputHandler"/>.
/// </summary>
public bool HandleKeyboard = true;
/// <summary>
/// Set this to false to disable touch input for these ui controls.
/// Note that this does not disable touch input for the underlying <see cref="InputHandler"/>.
/// </summary>
public bool HandleTouch = true;
/// <summary>
/// Set this to false to disable gamepad input for these ui controls.
/// Note that this does not disable gamepad input for the underlying <see cref="InputHandler"/>.
/// </summary>
public bool HandleGamepad = true;
public bool IsAutoNavMode;
/// <summary>
/// If this value is true, the ui controls are in automatic navigation mode.
/// This means that the <see cref="UiStyle.SelectionIndicator"/> will be drawn around the <see cref="SelectedElement"/>.
/// To set this value, use <see cref="SelectElement"/> or <see cref="RootElement.SelectElement"/>
/// </summary>
public bool IsAutoNavMode { get; internal set; }
/// <summary>
/// Creates a new instance of the ui controls.
/// You should rarely have to invoke this manually, since the <see cref="UiSystem"/> handles it.
/// </summary>
/// <param name="system">The ui system to control with these controls</param>
/// <param name="inputHandler">The input handler to use for controlling, or null to create a new one.</param>
public UiControls(UiSystem system, InputHandler inputHandler = null) {
this.System = system;
this.Input = inputHandler ?? new InputHandler();
@ -44,6 +130,9 @@ namespace MLEM.Ui {
InputHandler.EnableGestures(GestureType.Tap, GestureType.Hold);
}
/// <summary>
/// Update this ui controls instance, causing the underlying <see cref="InputHandler"/> to be updated, as well as ui input to be queried.
/// </summary>
public virtual void Update() {
if (this.IsInputOurs)
this.Input.Update();
@ -134,6 +223,13 @@ namespace MLEM.Ui {
}
}
/// <summary>
/// Returns the <see cref="Element"/> in the underlying <see cref="UiSystem"/> that is currently below the given position.
/// Throughout the ui system, this is used for mouse input querying.
/// </summary>
/// <param name="position">The position to query</param>
/// <param name="transform">If this value is true, the <see cref="RootElement.Transform"/> will be applied.</param>
/// <returns>The element under the position, or null if there isn't one</returns>
public virtual Element GetElementUnderPos(Vector2 position, bool transform = true) {
foreach (var root in this.System.GetRootElements()) {
var pos = transform ? Vector2.Transform(position, root.InvTransform) : position;
@ -144,6 +240,14 @@ namespace MLEM.Ui {
return null;
}
/// <summary>
/// Selects the given element that is a child of the given root element.
/// Optionally, automatic navigation can be forced on, causing the <see cref="UiStyle.SelectionIndicator"/> to be drawn around the element.
/// A simpler version of this method is <see cref="RootElement.SelectElement"/>.
/// </summary>
/// <param name="root">The root element of the <see cref="Element"/></param>
/// <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>
public void SelectElement(RootElement root, Element element, bool? autoNav = null) {
if (root == null)
return;
@ -165,6 +269,12 @@ namespace MLEM.Ui {
this.IsAutoNavMode = autoNav.Value;
}
/// <summary>
/// Returns the selected element for the given root element.
/// A property equivalent to this method is <see cref="RootElement.SelectedElement"/>.
/// </summary>
/// <param name="root">The root element whose selected element to return</param>
/// <returns>The given root's selected element, or null if the root doesn't exist, or if there is no selected element for that root.</returns>
public Element GetSelectedElement(RootElement root) {
if (root == null)
return null;
@ -172,6 +282,12 @@ namespace MLEM.Ui {
return element;
}
/// <summary>
/// Returns the next element to select when pressing the <see cref="Keys.Tab"/> key during keyboard navigation.
/// If the <c>backward</c> boolean is true, the previous element should be returned instead.
/// </summary>
/// <param name="backward">If we're going backwards (if <see cref="ModifierKey.Shift"/> is held)</param>
/// <returns>The next or previous element to select</returns>
protected virtual Element GetTabNextElement(bool backward) {
if (this.ActiveRoot == null)
return null;
@ -200,6 +316,11 @@ namespace MLEM.Ui {
}
}
/// <summary>
/// Returns the next element that should be selected during gamepad navigation, based on the <see cref="RectangleF"/> that we're looking for elements in.
/// </summary>
/// <param name="searchArea">The area that we're looking for next elements in</param>
/// <returns>The first element found in that area</returns>
protected virtual Element GetGamepadNextElement(RectangleF searchArea) {
if (this.ActiveRoot == null)
return null;
@ -256,6 +377,11 @@ namespace MLEM.Ui {
this.SelectElement(this.ActiveRoot, next);
}
/// <summary>
/// A helper function to add <see cref="Keys"/>, <see cref="Buttons"/> or <see cref="MouseButton"/> to an array of controls.
/// </summary>
/// <param name="controls">The controls to add to</param>
/// <param name="additional">The additional controls to add to the controls list</param>
public static void AddButtons(ref object[] controls, params object[] additional) {
controls = controls.Concat(additional).ToArray();
}

View file

@ -16,16 +16,42 @@ using MLEM.Ui.Elements;
using MLEM.Ui.Style;
namespace MLEM.Ui {
/// <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.
/// For more information on how ui systems work, check out <see href="https://mlem.ellpeck.de/articles/ui.html"/>
/// </summary>
public class UiSystem : GameComponent {
/// <summary>
/// The graphics device that this ui system uses for its size calculations
/// </summary>
public readonly GraphicsDevice GraphicsDevice;
/// <summary>
/// The game window that this ui system renders within
/// </summary>
public readonly GameWindow Window;
private readonly List<RootElement> rootElements = new List<RootElement>();
/// <summary>
/// The viewport that this ui system is rendering inside of.
/// This is automatically updated during <see cref="GameWindow.ClientSizeChanged"/>
/// </summary>
public Rectangle Viewport { get; private set; }
/// <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>
public bool AutoScaleWithScreen;
/// <summary>
/// If <see cref="AutoScaleWithScreen"/> is true, this is used as the screen size that uses the default <see cref="GlobalScale"/>
/// </summary>
public Point AutoScaleReferenceSize;
private float globalScale = 1;
/// <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>
public float GlobalScale {
get {
if (!this.AutoScaleWithScreen)
@ -40,6 +66,10 @@ namespace MLEM.Ui {
}
private UiStyle style;
/// <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>
public UiStyle Style {
get => this.style;
set {
@ -50,27 +80,94 @@ namespace MLEM.Ui {
}
}
}
/// <summary>
/// The transparency (alpha value) that this ui system and all of its elements draw at.
/// </summary>
public float DrawAlpha = 1;
/// <summary>
/// The blend state that this ui system and all of its elements draw with
/// </summary>
public BlendState BlendState;
/// <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>
public SamplerState SamplerState = SamplerState.PointClamp;
/// <summary>
/// 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>
public TextFormatter TextFormatter;
/// <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>
public UiControls Controls;
/// <summary>
/// Event that is invoked after an <see cref="Element"/> is drawn, but before its children are drawn.
/// </summary>
public Element.DrawCallback OnElementDrawn = (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
/// <summary>
/// Event that is invoked after the <see cref="RootElement.SelectedElement"/> for each root element is drawn, but before its children are drawn.
/// </summary>
public Element.DrawCallback OnSelectedElementDrawn;
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is updated
/// </summary>
public Element.TimeCallback OnElementUpdated = (e, time) => e.OnUpdated?.Invoke(e, time);
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the primary action key
/// </summary>
public Element.GenericCallback OnElementPressed = e => e.OnPressed?.Invoke(e);
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is pressed with the secondary action key
/// </summary>
public Element.GenericCallback OnElementSecondaryPressed = e => e.OnSecondaryPressed?.Invoke(e);
/// <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>
public Element.GenericCallback OnElementSelected = e => e.OnSelected?.Invoke(e);
/// <summary>
/// Event that is invoked when an <see cref="Element"/> is deselected during the selection of a new element.
/// </summary>
public Element.GenericCallback OnElementDeselected = e => e.OnDeselected?.Invoke(e);
/// <summary>
/// Event that is invoked when the mouse enters an <see cref="Element"/>
/// </summary>
public Element.GenericCallback OnElementMouseEnter = e => e.OnMouseEnter?.Invoke(e);
/// <summary>
/// Event that is invoked when the mouse exits an <see cref="Element"/>
/// </summary>
public Element.GenericCallback OnElementMouseExit = e => e.OnMouseExit?.Invoke(e);
/// <summary>
/// Event that is invoked when an <see cref="Element"/>'s display area changes
/// </summary>
public Element.GenericCallback OnElementAreaUpdated = e => e.OnAreaUpdated?.Invoke(e);
/// <summary>
/// Event that is invoked when the <see cref="Element"/> that the mouse is currently over changes
/// </summary>
public Element.GenericCallback OnMousedElementChanged;
/// <summary>
/// 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>
public Element.GenericCallback OnSelectedElementChanged;
/// <summary>
/// Event that is invoked when a new <see cref="RootElement"/> is added to this ui system
/// </summary>
public RootCallback OnRootAdded;
/// <summary>
/// Event that is invoked when a <see cref="RootElement"/> is removed from this ui system
/// </summary>
public RootCallback OnRootRemoved;
/// <summary>
/// Creates a new ui system with the given settings.
/// </summary>
/// <param name="window">The game's window</param>
/// <param name="device">The graphics device that should be used for viewport calculations</param>
/// <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>
public UiSystem(GameWindow window, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) : base(null) {
this.Controls = new UiControls(this, inputHandler);
this.GraphicsDevice = device;
@ -106,6 +203,10 @@ namespace MLEM.Ui {
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 link && link.Token == t));
}
/// <summary>
/// Update this ui system, querying the necessary events and updating each element's data.
/// </summary>
/// <param name="time">The game's time</param>
public override void Update(GameTime time) {
this.Controls.Update();
@ -114,6 +215,12 @@ namespace MLEM.Ui {
}
}
/// <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>
public void DrawEarly(GameTime time, SpriteBatch batch) {
foreach (var root in this.rootElements) {
if (!root.Element.IsHidden)
@ -121,6 +228,12 @@ namespace MLEM.Ui {
}
}
/// <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>
public void Draw(GameTime time, SpriteBatch batch) {
foreach (var root in this.rootElements) {
if (root.Element.IsHidden)
@ -132,6 +245,13 @@ namespace MLEM.Ui {
}
}
/// <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>
public RootElement Add(string name, Element element) {
if (this.IndexOf(name) >= 0)
return null;
@ -148,6 +268,10 @@ namespace MLEM.Ui {
return root;
}
/// <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>
public void Remove(string name) {
var root = this.Get(name);
if (root == null)
@ -163,6 +287,11 @@ namespace MLEM.Ui {
this.OnRootRemoved?.Invoke(root);
}
/// <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>
public RootElement Get(string name) {
var index = this.IndexOf(name);
return index < 0 ? null : this.rootElements[index];
@ -179,26 +308,58 @@ namespace MLEM.Ui {
this.rootElements.AddRange(sorted);
}
/// <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>
public IEnumerable<RootElement> GetRootElements() {
for (var i = this.rootElements.Count - 1; i >= 0; i--)
yield return this.rootElements[i];
}
/// <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>
public void ApplyToAll(Action<Element> action) {
foreach (var root in this.rootElements)
root.Element.AndChildren(action);
}
/// <summary>
/// A delegate used for callbacks that involve a <see cref="RootElement"/>
/// </summary>
/// <param name="root">The root element</param>
public delegate void RootCallback(RootElement root);
}
/// <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>
public class RootElement {
/// <summary>
/// The name of this root element
/// </summary>
public readonly string Name;
/// <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>
public readonly Element Element;
/// <summary>
/// The <see cref="UiSystem"/> that this root element is a part of.
/// </summary>
public readonly UiSystem System;
private float scale = 1;
/// <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>
public float Scale {
get => this.scale;
set {
@ -209,6 +370,10 @@ namespace MLEM.Ui {
}
}
private int priority;
/// <summary>
/// The priority of this root element.
/// A higher priority means the element will be updated first, as well as rendered on top.
/// </summary>
public int Priority {
get => this.priority;
set {
@ -216,18 +381,43 @@ namespace MLEM.Ui {
this.System.SortRoots();
}
}
/// <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>
public float ActualScale => this.System.GlobalScale * this.Scale;
/// <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>
public Matrix Transform = Matrix.Identity;
/// <summary>
/// An inversion of <see cref="Transform"/>
/// </summary>
public Matrix InvTransform => Matrix.Invert(this.Transform);
/// <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>
public Element SelectedElement => this.System.Controls.GetSelectedElement(this);
/// <summary>
/// Determines whether this root element contains any children that <see cref="Elements.Element.CanBeSelected"/>.
/// This value is automatically calculated.
/// </summary>
public bool CanSelectContent { get; private set; }
/// <summary>
/// Event that is invoked when a <see cref="Element"/> is added to this root element or any of its children.
/// </summary>
public Element.GenericCallback OnElementAdded;
/// <summary>
/// Even that is invoked when a <see cref="Element"/> is removed rom this root element of any of its children.
/// </summary>
public Element.GenericCallback OnElementRemoved;
public RootElement(string name, Element element, UiSystem system) {
internal RootElement(string name, Element element, UiSystem system) {
this.Name = name;
this.Element = element;
this.System = system;
@ -242,6 +432,12 @@ namespace MLEM.Ui {
};
}
/// <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>
public void SelectElement(Element element, bool? autoNav = null) {
this.System.Controls.SelectElement(this, element, autoNav);
}