diff --git a/MLEM.Ui/Anchor.cs b/MLEM.Ui/Anchor.cs index ddf3cb7..e07ed5f 100644 --- a/MLEM.Ui/Anchor.cs +++ b/MLEM.Ui/Anchor.cs @@ -1,21 +1,74 @@ +using MLEM.Ui.Elements; + namespace MLEM.Ui { + /// + /// Represents a location for an to attach to within its parent (or within the screen's viewport if it is the ). + /// public enum Anchor { + /// + /// Attach to the top left corner of the parent + /// TopLeft, + /// + /// Attach to the center of the top edge of the parent + /// TopCenter, + /// + /// Attach to the top right corner of the parent + /// TopRight, + /// + /// Attach to the center of the left edge of the parent + /// CenterLeft, + /// + /// Attach to the center position of the parent + /// Center, + /// + /// Attach to the center of the right edge of the parent + /// CenterRight, + /// + /// Attach to the bottom left corner of the parent + /// BottomLeft, + /// + /// Attach to the center of the bottom edge of the parent + /// BottomCenter, + /// + /// Attach to the bottom right corner of the parent + /// 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 + /// + /// 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. + /// + AutoLeft, + /// + /// 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. + /// + AutoCenter, + /// + /// 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. + /// + AutoRight, + /// + /// 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. + /// + AutoInline, + /// + /// This is an auto-anchoring value. + /// This anchor is an overflow-ignoring version of , 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. + /// + AutoInlineIgnoreOverflow } } \ No newline at end of file diff --git a/MLEM.Ui/Elements/Button.cs b/MLEM.Ui/Elements/Button.cs index 87ee447..00f2fa0 100644 --- a/MLEM.Ui/Elements/Button.cs +++ b/MLEM.Ui/Elements/Button.cs @@ -5,18 +5,54 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A button element for use inside of a . + /// A button element can be pressed, hovered over and that can be disabled. + /// public class Button : Element { + /// + /// The button's texture + /// public StyleProp Texture; + /// + /// The color that the button draws its texture with + /// public StyleProp NormalColor = Color.White; + /// + /// The texture that the button uses while being moused over. + /// If this is null, it uses its default . + /// public StyleProp HoveredTexture; + /// + /// The color that the button uses for drawing while being moused over + /// public StyleProp HoveredColor; + /// + /// The texture that the button uses when it . + /// If this is null, it uses its default . + /// public StyleProp DisabledTexture; + /// + /// The color that the button uses for drawing when it + /// public StyleProp DisabledColor; + /// + /// The of text that is displayed on the button. + /// Note that this is only nonnull by default if the constructor was passed a nonnull text. + /// public Paragraph Text; + /// + /// The 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. + /// public Tooltip Tooltip; private bool isDisabled; + /// + /// Set this property to true to mark the button as disabled. + /// A disabled button cannot be moused over, selected or pressed. + /// public bool IsDisabled { get => this.isDisabled; set { @@ -26,6 +62,14 @@ namespace MLEM.Ui.Elements { } } + /// + /// Creates a new button with the given settings + /// + /// The button's anchor + /// The button's size + /// The text that should be displayed on the button + /// The text that should be displayed in a when hovering over this button + /// The width of this button's , or 50 by default 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); } + /// 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); } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture.SetFromStyle(style.ButtonTexture); diff --git a/MLEM.Ui/Elements/Checkbox.cs b/MLEM.Ui/Elements/Checkbox.cs index 783f812..42ebabe 100644 --- a/MLEM.Ui/Elements/Checkbox.cs +++ b/MLEM.Ui/Elements/Checkbox.cs @@ -7,16 +7,43 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A checkbox element to use inside of a . + /// 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 . + /// public class Checkbox : Element { + /// + /// The texture that this checkbox uses for drawing + /// public StyleProp Texture; + /// + /// The texture that this checkbox uses when it is hovered. + /// If this is null, the default is used. + /// public StyleProp HoveredTexture; + /// + /// The color that this checkbox uses for drawing when it is hovered. + /// public StyleProp HoveredColor; + /// + /// The texture that is rendered on top of this checkbox when it is . + /// public StyleProp Checkmark; + /// + /// The label that displays next to this checkbox + /// public Paragraph Label; + /// + /// The width of the space between this checkbox and its + /// public float TextOffsetX = 2; private bool checced; + /// + /// Whether or not this checkbox is currently checked. + /// public bool Checked { get => this.checced; set { @@ -26,8 +53,18 @@ namespace MLEM.Ui.Elements { } } } + /// + /// An event that is invoked when this checkbox's property changes + /// public CheckStateChange OnCheckStateChange; + /// + /// Creates a new checkbox with the given settings + /// + /// The checkbox's anchor + /// The checkbox's size + /// The checkbox's label text + /// The default value of 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 { } } + /// 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; } + /// 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); } + /// 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); } + /// + /// A delegate used for + /// + /// The checkbox whose checked state changed + /// The new value of public delegate void CheckStateChange(Checkbox box, bool checced); } diff --git a/MLEM.Ui/Elements/CustomDrawGroup.cs b/MLEM.Ui/Elements/CustomDrawGroup.cs index 8220975..bf53125 100644 --- a/MLEM.Ui/Elements/CustomDrawGroup.cs +++ b/MLEM.Ui/Elements/CustomDrawGroup.cs @@ -3,12 +3,26 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MLEM.Ui.Elements { + /// + /// A that can have custom drawing parameters. + /// Custom drawing parameters include a matrix, as well as a custom call. + /// All of the custom draw group will be drawn with the custom parameters. + /// public class CustomDrawGroup : Group { + /// + /// This custom draw group's transform matrix + /// public Matrix? Transform; + /// + /// A callback for retrieving this group's automatically + /// public TransformCallback TransformGetter; private BeginDelegate beginImpl; private bool isDefaultBegin; + /// + /// The call that this custom draw group should make to to begin drawing. + /// public BeginDelegate BeginImpl { get => this.beginImpl; set { @@ -17,12 +31,21 @@ namespace MLEM.Ui.Elements { } } + /// + /// Creates a new custom draw group with the given settings + /// + /// The group's anchor + /// The group's size + /// The group's + /// The group's + /// Whether this group should automatically calculate its height based on its children 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; } + /// 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 { } } + /// + /// Scales this custom draw group's matrix based on the given scale and origin. + /// + /// The scale to use + /// The origin to use for scaling, or null to use this element's center point public void ScaleOrigin(float scale, Vector2? origin = null) { this.Transform = Matrix.CreateScale(scale, scale, 0) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.DisplayArea.Center), 0)); } + /// + /// A delegate method used for + /// + /// The custom draw group + /// The game's time + /// The sprite batch used for drawing + /// This element's draw alpha + /// The blend state used for drawing + /// The sampler state used for drawing + /// The transform matrix used for drawing public delegate void BeginDelegate(CustomDrawGroup element, GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix); + /// + /// A delegate method used for + /// + /// The element whose transform to get + /// The game's time + /// The regular transform matrix public delegate Matrix TransformCallback(CustomDrawGroup element, GameTime time, Matrix matrix); } diff --git a/MLEM.Ui/Elements/Dropdown.cs b/MLEM.Ui/Elements/Dropdown.cs index 79cb538..54ef10e 100644 --- a/MLEM.Ui/Elements/Dropdown.cs +++ b/MLEM.Ui/Elements/Dropdown.cs @@ -3,9 +3,19 @@ using Microsoft.Xna.Framework; using MLEM.Misc; namespace MLEM.Ui.Elements { + /// + /// A dropdown component to use inside of a . + /// A dropdown is a component that contains a hidden panel which is displayed upon pressing the dropdown button. + /// public class Dropdown : Button { + /// + /// The panel that this dropdown contains. It will be displayed upon pressing the dropdown button. + /// public readonly Panel Panel; + /// + /// This property stores whether the dropdown is currently opened or not + /// public bool IsOpen { get => !this.Panel.IsHidden; set { @@ -13,8 +23,19 @@ namespace MLEM.Ui.Elements { this.OnOpenedOrClosed?.Invoke(this); } } + /// + /// An event that is invoked when changes + /// public GenericCallback OnOpenedOrClosed; + /// + /// Creates a new dropdown with the given settings + /// + /// The dropdown's anchor + /// The dropdown button's size + /// The text displayed on the dropdown button + /// The text displayed as a tooltip when hovering over the dropdown button + /// The width of the dropdown button's tooltip 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; } + /// + /// Adds an element to this dropdown's + /// + /// The element to add 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 { }; } + /// + /// Adds a pressable element to this dropdown's + /// + /// The text to display + /// The resulting paragraph's event public void AddElement(string text, GenericCallback pressed = null) { this.AddElement(p => text, pressed); } + /// + /// Adds a pressable element to this dropdown's . + /// By default, the paragraph's text color will change from to when hovering over it. + /// + /// The text to display + /// The resulting paragraph's event public void AddElement(Paragraph.TextCallback text, GenericCallback pressed = null) { var paragraph = new Paragraph(Anchor.AutoLeft, 1, text) { CanBeMoused = true, diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index f2089d1..f41ec84 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -13,10 +13,20 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// This class represents a generic base class for ui elements of a . + /// public abstract class Element : GenericDataHolder { + /// + /// A list of all of this element's direct children. + /// Use or to manipulate this list while calling all of the necessary callbacks. + /// protected readonly List Children = new List(); private readonly List sortedChildren = new List(); + /// + /// A sorted version of . The children are sorted by their . + /// protected List SortedChildren { get { this.UpdateSortedChildrenIfDirty(); @@ -26,6 +36,9 @@ namespace MLEM.Ui.Elements { private bool sortedChildrenDirty; private UiSystem system; + /// + /// The ui system that this element is currently a part of + /// public UiSystem System { get => this.system; internal set { @@ -35,13 +48,33 @@ namespace MLEM.Ui.Elements { this.InitStyle(this.system.Style); } } + /// + /// The controls that this element's uses + /// public UiControls Controls; + /// + /// The input handler that this element's use + /// protected InputHandler Input => this.Controls.Input; + /// + /// This element's parent element. + /// If this element has no parent (it is the of a ui system), this value is null. + /// public Element Parent { get; private set; } + /// + /// This element's . + /// Note that this value is set even if this element has a . To get the element that represents the root element, use . + /// public RootElement Root { get; internal set; } + /// + /// The scale that this ui element renders with + /// public float Scale => this.Root.ActualScale; private Anchor anchor; + /// + /// The that this element uses for positioning within its parent + /// public Anchor Anchor { get => this.anchor; set { @@ -53,6 +86,10 @@ namespace MLEM.Ui.Elements { } private Vector2 size; + /// + /// 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. + /// public Vector2 Size { get => this.size; set { @@ -62,9 +99,16 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } } + /// + /// The , but with applied. + /// public Vector2 ScaledSize => this.size * this.Scale; private Vector2 offset; + /// + /// This element's offset from its default position, which is dictated by its . + /// Note that, depending on the side that the element is anchored to, this offset moves it in a different direction. + /// public Vector2 PositionOffset { get => this.offset; set { @@ -74,12 +118,26 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } } + /// + /// The , but with applied. + /// public Vector2 ScaledOffset => this.offset * this.Scale; + /// + /// The padding that this element has. + /// The padding is subtracted from the element's , and it is an area that the element does not extend into. This means that this element's resulting does not include this padding. + /// public Padding Padding; + /// + /// The , but with applied. + /// public Padding ScaledPadding => this.Padding * this.Scale; private Padding childPadding; + /// + /// The child padding that this element has. + /// The child padding moves any added to this element inwards by the given amount in each direction. + /// public Padding ChildPadding { get => this.childPadding; set { @@ -89,10 +147,20 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } } + /// + /// The , but with applied. + /// public Padding ScaledChildPadding => this.childPadding * this.Scale; + /// + /// This element's current , but with applied. + /// public RectangleF ChildPaddedArea => this.UnscrolledArea.Shrink(this.ScaledChildPadding); private RectangleF area; + /// + /// This element's area, without respecting its . + /// This area is updated automatically to fit this element's sizing and positioning properties. + /// public RectangleF UnscrolledArea { get { this.UpdateAreaIfDirty(); @@ -100,13 +168,30 @@ namespace MLEM.Ui.Elements { } } private bool areaDirty; + /// + /// The of this element, but with applied. + /// public RectangleF Area => this.UnscrolledArea.OffsetCopy(this.ScaledScrollOffset); + /// + /// The area that this element is displayed in, which is shrunk by this element's . + /// This is the property that should be used for drawing this element, as well as mouse input handling and culling. + /// public RectangleF DisplayArea => this.Area.Shrink(this.ScaledPadding); + /// + /// The offset that this element has as a result of scrolling. + /// public Vector2 ScrollOffset; + /// + /// The , but with applied. + /// public Vector2 ScaledScrollOffset => this.ScrollOffset * this.Scale; private bool isHidden; + /// + /// Set this property to true to cause this element to be hidden. + /// Hidden elements don't receive input events, aren't rendered and don't factor into auto-anchoring. + /// public bool IsHidden { get => this.isHidden; set { @@ -118,6 +203,10 @@ namespace MLEM.Ui.Elements { } private int priority; + /// + /// The priority of this element as part of its element. + /// A higher priority means the element will be drawn first and, if auto-anchoring is used, anchored higher up within its parent. + /// public int Priority { get => this.priority; set { @@ -127,39 +216,136 @@ namespace MLEM.Ui.Elements { } } + /// + /// Set this field to false to disallow the element from being selected. + /// An unselectable element is skipped by automatic navigation and its callback will never be called. + /// public bool CanBeSelected = true; + /// + /// Set this field to false to disallow the element from reacting to being moused over. + /// public bool CanBeMoused = true; + /// + /// Set this field to false to disallow this element's and events to be called. + /// public bool CanBePressed = true; + /// + /// Set this field to false to cause auto-anchored siblings to ignore this element as a possible anchor point. + /// public bool CanAutoAnchorsAttach = true; + /// + /// Set this field to true to cause this element's height to be automatically calculated based on the area that its take up. + /// public bool SetHeightBasedOnChildren; + /// + /// Set this field to true to cause this element's width to be automatically calculated based on the area that is take up. + /// public bool SetWidthBasedOnChildren; + /// + /// The transparency (alpha value) that this element is rendered with. + /// Note that, when is called, this alpha value is multiplied with the 's alpha value and passed down to this element's . + /// public float DrawAlpha = 1; + /// + /// Stores whether this element is currently being moused over. + /// public bool IsMouseOver { get; private set; } + /// + /// Stores whether this element is its 's . + /// public bool IsSelected { get; private set; } + /// + /// Event that is called after this element is drawn, but before its children are drawn + /// public DrawCallback OnDrawn; + /// + /// Event that is called when this element is updated + /// public TimeCallback OnUpdated; + /// + /// Event that is called when this element is pressed + /// public GenericCallback OnPressed; + /// + /// Event that is called when this element is pressed using the secondary action + /// public GenericCallback OnSecondaryPressed; + /// + /// Event that is called when this element's is turned true + /// public GenericCallback OnSelected; + /// + /// Event that is called when this element's is turned false + /// public GenericCallback OnDeselected; + /// + /// Event that is called when this element starts being moused over + /// public GenericCallback OnMouseEnter; + /// + /// Event that is called when this element stops being moused over + /// public GenericCallback OnMouseExit; + /// + /// Event that is called when text input is made. + /// Note that this event is called for every element, even if it is not selected. + /// Also note that if is true, this event is never called. + /// public TextInputCallback OnTextInput; + /// + /// Event that is called when this element's is changed. + /// public GenericCallback OnAreaUpdated; + /// + /// Event that is called when the element that is currently being moused changes within the ui system. + /// Note that the event fired doesn't necessarily correlate to this specific element. + /// public OtherElementCallback OnMousedElementChanged; + /// + /// Event that is called when the element that is currently selected changes within the ui system. + /// Note that the event fired doesn't necessarily correlate to this specific element. + /// public OtherElementCallback OnSelectedElementChanged; + /// + /// Event that is called when the next element to select when pressing tab is calculated. + /// To cause a different element than the default one to be selected, return it during this event. + /// public TabNextElementCallback GetTabNextElement; + /// + /// Event that is called when the next element to select when using gamepad input is calculated. + /// To cause a different element than the default one to be selected, return it during this event. + /// public GamepadNextElementCallback GetGamepadNextElement; + /// + /// Event that is called when a child is added to this element using + /// public OtherElementCallback OnChildAdded; + /// + /// Event that is called when a child is removed from this element using + /// public OtherElementCallback OnChildRemoved; + /// + /// A style property that contains the selection indicator that is displayed on this element if it is the + /// public StyleProp SelectionIndicator; + /// + /// A style property that contains the sound effect that is played when this element's is called + /// public StyleProp ActionSound; + /// + /// A style property that contains the sound effect that is played when this element's is called + /// public StyleProp SecondActionSound; - public Element(Anchor anchor, Vector2 size) { + /// + /// Creates a new element with the given anchor and size and sets up some default event reactions. + /// + /// This element's + /// This element's default + protected Element(Anchor anchor, Vector2 size) { this.anchor = anchor; this.size = size; @@ -173,6 +359,13 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } + /// + /// Adds a child to this element. + /// + /// The child element to add + /// The index to add the child at, or -1 to add it to the end of the list + /// The type of child to add + /// This element, for chaining public T AddChild(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; } + /// + /// Removes the given child from this element. + /// + /// The child element to remove 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(); } + /// + /// Removes all children from this element that match the given condition. + /// + /// The condition that determines if a child should be removed public void RemoveChildren(Func 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 { } } + /// + /// Causes to be recalculated as soon as possible. + /// public void SetSortedChildrenDirty() { this.sortedChildrenDirty = true; } + /// + /// Updates the list if is true. + /// public void UpdateSortedChildrenIfDirty() { if (this.sortedChildrenDirty) this.ForceUpdateSortedChildren(); } + /// + /// Forces an update of the list. + /// public virtual void ForceUpdateSortedChildren() { this.sortedChildrenDirty = false; @@ -230,17 +440,28 @@ namespace MLEM.Ui.Elements { this.sortedChildren.Sort((e1, e2) => e1.Priority.CompareTo(e2.Priority)); } + /// + /// Causes this element's to be recalculated as soon as possible. + /// If this element is auto-anchored or its parent automatically changes its size based on its children, this element's parent's area is also marked dirty. + /// public void SetAreaDirty() { this.areaDirty = true; if (this.Parent != null && (this.Anchor >= Anchor.AutoLeft || this.Parent.SetWidthBasedOnChildren || this.Parent.SetHeightBasedOnChildren)) this.Parent.SetAreaDirty(); } + /// + /// Updates this element's list if is true. + /// public void UpdateAreaIfDirty() { if (this.areaDirty) this.ForceUpdateArea(); } + /// + /// Forces this element's to be updated if it is not . + /// This method also updates all of this element's 's areas. + /// public virtual void ForceUpdateArea() { this.areaDirty = false; if (this.IsHidden) @@ -362,16 +583,30 @@ namespace MLEM.Ui.Elements { } } + /// + /// Calculates the actual size that this element should take up, based on the area that its parent encompasses. + /// + /// This parent's area, or the ui system's viewport if it has no parent + /// The actual size of this element, taking into account protected virtual Vector2 CalcActualSize(RectangleF parentArea) { 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); } + /// + /// Returns the area that should be used for determining where auto-anchoring children should attach. + /// + /// The area for auto anchors protected virtual RectangleF GetAreaForAutoAnchors() { return this.UnscrolledArea; } + /// + /// Returns this element's lowest child element (in terms of y position) that matches the given condition. + /// + /// The condition to match + /// The lowest element, or null if no such element exists public Element GetLowestChild(Func condition = null) { Element lowest = null; foreach (var child in this.Children) { @@ -385,6 +620,11 @@ namespace MLEM.Ui.Elements { return lowest; } + /// + /// Returns this element's rightmost child (in terms of x position) that matches the given condition. + /// + /// The condition to match + /// The rightmost element, or null if no such element exists public Element GetRightmostChild(Func condition = null) { Element rightmost = null; foreach (var child in this.Children) { @@ -398,6 +638,12 @@ namespace MLEM.Ui.Elements { return rightmost; } + /// + /// Returns this element's lowest sibling that is also older (lower in its parent's list) than this element that also matches the given condition. + /// The returned element's will always be equal to this element's . + /// + /// The condition to match + /// The lowest older sibling of this element, or null if no such element exists public Element GetLowestOlderSibling(Func condition = null) { if (this.Parent == null) return null; @@ -413,6 +659,12 @@ namespace MLEM.Ui.Elements { return lowest; } + /// + /// Returns this element's first older sibling that matches the given condition. + /// The returned element's will always be equal to this element's . + /// + /// The condition to match + /// The older sibling, or null if no such element exists public Element GetOlderSibling(Func condition = null) { if (this.Parent == null) return null; @@ -427,6 +679,12 @@ namespace MLEM.Ui.Elements { return older; } + /// + /// Returns all of this element's siblings that match the given condition. + /// Siblings are elements that have the same as this element. + /// + /// The condition to match + /// This element's siblings public IEnumerable GetSiblings(Func condition = null) { if (this.Parent == null) yield break; @@ -438,6 +696,15 @@ namespace MLEM.Ui.Elements { } } + /// + /// Returns all of this element's children of the given type that match the given condition. + /// Optionally, the entire tree of children (grandchildren) can be searched. + /// + /// The condition to match + /// If this value is true, children of children of this element are also searched + /// If this value is true, children for which the condition does not match will not have their children searched + /// The type of children to search for + /// All children that match the condition public IEnumerable GetChildren(Func condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) where T : Element { foreach (var child in this.Children) { var applies = child is T t && (condition == null || condition(t)); @@ -450,10 +717,16 @@ namespace MLEM.Ui.Elements { } } + /// public IEnumerable GetChildren(Func condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) { return this.GetChildren(condition, regardGrandchildren, ignoreFalseGrandchildren); } + /// + /// Returns the parent tree of this element. + /// The parent tree is this element's , followed by its parent, and so on, up until the 's . + /// + /// This element's parent tree public IEnumerable GetParentTree() { if (this.Parent == null) yield break; @@ -462,10 +735,19 @@ namespace MLEM.Ui.Elements { yield return parent; } + /// + /// Returns a subset of that are currently relevant in terms of drawing and input querying. + /// A only returns elements that are currently in view here. + /// + /// This element's relevant children protected virtual List GetRelevantChildren() { return this.SortedChildren; } + /// + /// Updates this element and all of its + /// + /// The game's time public virtual void Update(GameTime time) { this.System.OnElementUpdated?.Invoke(this, time); @@ -474,6 +756,16 @@ namespace MLEM.Ui.Elements { child.Update(time); } + /// + /// Draws this element and all of its + /// Note that, when this is called, has already been called. + /// + /// The game's time + /// The sprite batch to use for drawing + /// The alpha to draw this element and its children with + /// The blend state that is used for drawing + /// The sampler state that is used for drawing + /// The transformation matrix that is used for drawing 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 { } } + /// + /// Draws this element and all of its early. + /// Drawing early involves drawing onto instances rather than onto the screen. + /// Note that, when this is called, has not yet been called. + /// + /// The game's time + /// The sprite batch to use for drawing + /// The alpha to draw this element and its children with + /// The blend state that is used for drawing + /// The sampler state that is used for drawing + /// The transformation matrix that is used for drawing 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 { } } + /// + /// Returns the element under the given position, searching the current element and all of its . + /// + /// The position to query + /// The element under the position, or null if no such element exists public virtual Element GetElementUnderPos(Vector2 position) { if (this.IsHidden) return null; @@ -504,38 +812,92 @@ namespace MLEM.Ui.Elements { return this.CanBeMoused && this.DisplayArea.Contains(position) ? this : null; } + /// + /// Performs the specified action on this element and all of its + /// + /// The action to perform public void AndChildren(Action action) { action(this); foreach (var child in this.Children) child.AndChildren(action); } + /// + /// Sorts this element's using the given comparison. + /// + /// The comparison to sort by public void ReorderChildren(Comparison comparison) { this.Children.Sort(comparison); } + /// + /// Reverses this element's list in the given range. + /// + /// The index to start reversing at + /// The amount of elements to reverse public void ReverseChildren(int index = 0, int? count = null) { this.Children.Reverse(index, count ?? this.Children.Count); } + /// + /// Initializes this element's instances using the ui system's . + /// + /// The new style protected virtual void InitStyle(UiStyle style) { this.SelectionIndicator.SetFromStyle(style.SelectionIndicator); this.ActionSound.SetFromStyle(style.ActionSound?.CreateInstance()); this.SecondActionSound.SetFromStyle(style.ActionSound?.CreateInstance()); } + /// + /// A delegate used for the event. + /// + /// The current element + /// The key that was pressed + /// The character that was input public delegate void TextInputCallback(Element element, Keys key, char character); + /// + /// A generic element-specific delegate. + /// + /// The current element public delegate void GenericCallback(Element element); + /// + /// A generic element-specific delegate that includes a second element. + /// + /// The current element + /// The other element public delegate void OtherElementCallback(Element thisElement, Element otherElement); + /// + /// A delegate used inside of + /// + /// The element that is being drawn + /// The game's time + /// The sprite batch used for drawing + /// The alpha this element is drawn with public delegate void DrawCallback(Element element, GameTime time, SpriteBatch batch, float alpha); + /// + /// A generic delegate used inside of + /// + /// The current element + /// The game's time public delegate void TimeCallback(Element element, GameTime time); + /// + /// A delegate used by . + /// + /// If this value is true, is being held + /// The element that is considered to be the next element by default public delegate Element TabNextElementCallback(bool backward, Element usualNext); + /// + /// A delegate used by . + /// + /// The direction of the gamepad button that was pressed + /// The element that is considered to be the next element by default public delegate Element GamepadNextElementCallback(Direction2 dir, Element usualNext); } diff --git a/MLEM.Ui/Elements/ElementHelper.cs b/MLEM.Ui/Elements/ElementHelper.cs index 8025fa5..6830904 100644 --- a/MLEM.Ui/Elements/ElementHelper.cs +++ b/MLEM.Ui/Elements/ElementHelper.cs @@ -3,8 +3,22 @@ using MLEM.Input; using MLEM.Textures; namespace MLEM.Ui.Elements { + /// + /// This class contains a set of helper methods that aid in creating special kinds of compound types for use inside of a . + /// public static class ElementHelper { + /// + /// Creates a button with an image on the left side of its text. + /// + /// The button's anchor + /// The button's size + /// The texture of the image to render on the button + /// The text to display on the button + /// The text of the button's tooltip + /// The width of the button's tooltip + /// The of the button's image + /// An image button 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; } + /// + /// 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. + /// + /// The ui system to add the panel to, optional. + /// The anchor of the panel + /// The width of the panel + /// The text to display on the panel + /// The height of the "close" button + /// The text on the "close" button + /// An info box panel 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; } + /// + /// Creates an array of groups with a fixed width that can be used to create a column structure + /// + /// The element the groups should be added to, optional. + /// The total width of all of the groups combined + /// The amount of groups to split the total size into + /// Whether the groups should set their heights based on their children's heights + /// An array of columns 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; } + /// + /// Creates a with a + and a - button next to it, to allow for easy number input. + /// + /// The text field's anchor + /// The size of the text field + /// The default content of the text field + /// The value that is added or removed to the text field's value when clicking the + or - buttons + /// The rule for text input. by default. + /// A callback that is invoked when the text field's text changes + /// A group that contains the number field 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); diff --git a/MLEM.Ui/Elements/Group.cs b/MLEM.Ui/Elements/Group.cs index ea38bc9..5d31cab 100644 --- a/MLEM.Ui/Elements/Group.cs +++ b/MLEM.Ui/Elements/Group.cs @@ -2,13 +2,24 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MLEM.Ui.Elements { + /// + /// A group element to be used inside of a . + /// A group is an element that has no rendering or interaction on its own, but that can aid with automatic placement of child elements. + /// public class Group : Element { + /// + /// Creates a new group with the given settings + /// + /// The group's anchor + /// The group's size + /// Whether the group's height should be based on its children's height public Group(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = true) : base(anchor, size) { this.SetHeightBasedOnChildren = setHeightBasedOnChildren; this.CanBeSelected = false; } + /// 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(); diff --git a/MLEM.Ui/Elements/Image.cs b/MLEM.Ui/Elements/Image.cs index 4cfe6ed..364a284 100644 --- a/MLEM.Ui/Elements/Image.cs +++ b/MLEM.Ui/Elements/Image.cs @@ -6,11 +6,25 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// An image element to be used inside of a . + /// An image is simply an element that displays a supplied and optionally allows for the texture region to remain at its original aspect ratio, regardless of the element's size. + /// public class Image : Element { + /// + /// The color to render the image at + /// public StyleProp Color; private TextureRegion texture; + /// + /// A callback to retrieve the that this image should render. + /// This can be used if the image changes frequently. + /// public TextureCallback GetTextureCallback; + /// + /// The texture that this should render + /// public TextureRegion Texture { get { if (this.GetTextureCallback != null) @@ -27,6 +41,9 @@ namespace MLEM.Ui.Elements { } } private bool scaleToImage; + /// + /// Whether this image element's should be based on the size of the given. + /// public bool ScaleToImage { get => this.scaleToImage; set { @@ -36,11 +53,32 @@ namespace MLEM.Ui.Elements { } } } + /// + /// Whether to cause the to be rendered at its proper aspect ratio. + /// If this is false, the image will be stretched according to this component's size. + /// public bool MaintainImageAspect = true; + /// + /// The that the texture should be rendered with + /// public SpriteEffects ImageEffects = SpriteEffects.None; + /// + /// The scale that the image should be rendered with + /// public Vector2 ImageScale = Vector2.One; + /// + /// 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. + /// public float ImageRotation; + /// + /// Creates a new image with the given settings + /// + /// The image's anchor + /// The image's size + /// The texture the image should render + /// Whether this image's size should be based on the texture's size 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; } + /// 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; } + /// protected override Vector2 CalcActualSize(RectangleF parentArea) { return this.Texture != null && this.scaleToImage ? this.Texture.Size.ToVector2() : base.CalcActualSize(parentArea); } + /// 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); } + /// + /// A delegate method used for + /// + /// The current image element public delegate TextureRegion TextureCallback(Image image); } diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index 851c123..d0be470 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -10,15 +10,38 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A panel element to be used inside of a . + /// 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 on construction, which causes all elements that don't fit into the panel to be hidden until scrolled to using a . + /// As this behavior is accomplished using a , scrolling panels need to have their methods called using . + /// public class Panel : Element { + /// + /// The texture that this panel should have, or null if it should be invisible. + /// public StyleProp Texture; + /// + /// The scroll bar that this panel contains. + /// This is only nonnull if is true. + /// public readonly ScrollBar ScrollBar; private readonly bool scrollOverflow; private RenderTarget2D renderTarget; private readonly List relevantChildren = new List(); private bool relevantChildrenDirty; + /// + /// Creates a new panel with the given settings. + /// + /// The panel's anchor + /// The panel's default size + /// The panel's offset from its anchor point + /// Whether the panel should automatically calculate its height based on its children's size + /// Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers + /// The size of the 's scroller + /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling 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 { } } + /// 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; } + /// public override void ForceUpdateSortedChildren() { base.ForceUpdateSortedChildren(); if (this.scrollOverflow) this.relevantChildrenDirty = true; } + /// protected override List GetRelevantChildren() { var relevant = base.GetRelevantChildren(); if (this.scrollOverflow) { @@ -113,6 +139,7 @@ namespace MLEM.Ui.Elements { return relevant; } + /// 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 { } } + /// 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 { } } + /// 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; } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture.SetFromStyle(style.PanelTexture); } + /// + /// Prepares the panel for auto-scrolling, creating the render target and setting up the scroll bar's maximum value. + /// protected virtual void ScrollSetup() { if (!this.scrollOverflow) return; diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index d3f9be1..4ad41d3 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -15,12 +15,21 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A paragraph element for use inside of a . + /// A paragraph is an element that contains text. + /// A paragraph's text can be formatted using the ui system's . + /// public class Paragraph : Element { private string text; private string splitText; [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public FormattingCodeCollection Formatting; + /// + /// The font that this paragraph draws text with. + /// To set its bold and italic font, use and . + /// public StyleProp RegularFont; [Obsolete("Use the new GenericFont.Bold and GenericFont.Italic instead")] public StyleProp BoldFont; @@ -28,10 +37,23 @@ namespace MLEM.Ui.Elements { public StyleProp ItalicFont; [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public StyleProp FormatSettings; + /// + /// The tokenized version of the + /// public TokenizedString TokenizedText { get; private set; } + /// + /// The color that the text will be rendered with + /// public StyleProp TextColor; + /// + /// The scale that the text will be rendered with + /// public StyleProp TextScale; + /// + /// The text to render inside of this paragraph. + /// Use if the text changes frequently. + /// public string Text { get { this.QueryTextCallback(); @@ -47,13 +69,27 @@ namespace MLEM.Ui.Elements { } } } + /// + /// If this paragraph should automatically adjust its width based on the width of the text within it + /// public bool AutoAdjustWidth; + /// + /// An event that gets called when this paragraph's is queried. + /// Use this event for setting this paragraph's text if it changes frequently. + /// 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; + /// + /// Creates a new paragraph with the given settings. + /// + /// The paragraph's anchor + /// The paragraph's width. Note that its height is automatically calculated. + /// The paragraph's text + /// Whether the paragraph's width should automatically be calculated based on the text within it. 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; } + /// 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; } + /// 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); } + /// public override void Update(GameTime time) { this.QueryTextCallback(); base.Update(time); @@ -95,6 +134,7 @@ namespace MLEM.Ui.Elements { this.TokenizedText.Update(time); } + /// 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); } + /// 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); } + /// + /// Parses this paragraph's into . + /// Additionally, this method adds any elements for tokenized links in the text. + /// + /// The paragraph's default size 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); } + /// + /// A delegate method used for + /// + /// The current paragraph public delegate string TextCallback(Paragraph paragraph); [Obsolete("Use the new text formatting system in MLEM.Formatting instead")] public delegate string TextModifier(string text); + /// + /// A link is a sub-element of the that is added onto it as a child for any tokens that contain , to make them selectable and clickable. + /// public class Link : Element { + /// + /// The token that this link represents + /// public readonly Token Token; + /// + /// The links that form a cluster for the given token. + /// This only contains more than one element if the tokenized string has previously been . + /// public readonly Link[] LinkCluster; + /// + /// Creates a new link element with the given settings + /// + /// The link's anchor + /// The token that this link represents + /// The size of the token + /// 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. 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 { }; } + /// 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 diff --git a/MLEM.Ui/Elements/ProgressBar.cs b/MLEM.Ui/Elements/ProgressBar.cs index 2466363..cf739ec 100644 --- a/MLEM.Ui/Elements/ProgressBar.cs +++ b/MLEM.Ui/Elements/ProgressBar.cs @@ -7,22 +7,62 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A progress bar element to use inside of a . + /// A progress bar is an element that fills up a bar based on a given percentage. + /// public class ProgressBar : Element { + /// + /// The background texture that this progress bar should render + /// public StyleProp Texture; + /// + /// The color that this progress bar's should render with + /// public StyleProp Color; + /// + /// The padding that this progress bar's should have. + /// The padding is the amount of pixels that the progress texture is away from the borders of the progress bar. + /// public StyleProp ProgressPadding; + /// + /// The texture that this progress bar's progress should render + /// public StyleProp ProgressTexture; + /// + /// The color that this progress bar's is rendered with. + /// public StyleProp ProgressColor; + /// + /// The direction that this progress bar goes in. + /// Note that only directions are supported. + /// public Direction2 Direction; + /// + /// The maximum value that this progress bar should be able to have. + /// public float MaxValue; private float currentValue; + /// + /// The current value that this progress bar has. + /// This value is always between 0 and . + /// public float CurrentValue { get => this.currentValue; set => this.currentValue = MathHelper.Clamp(value, 0, this.MaxValue); } + /// + /// Creates a new progress bar with the given settings + /// + /// The progress bar's anchor + /// The size of the progress bar + /// The direction that the progress bar goes into + /// The progress bar's maximum value + /// The progress bar's current value + /// If the provided direction is not 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; } + /// 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); } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture.SetFromStyle(style.ProgressBarTexture); diff --git a/MLEM.Ui/Elements/RadioButton.cs b/MLEM.Ui/Elements/RadioButton.cs index 61a4c9d..f7aeb65 100644 --- a/MLEM.Ui/Elements/RadioButton.cs +++ b/MLEM.Ui/Elements/RadioButton.cs @@ -3,10 +3,26 @@ using MLEM.Input; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A radio button element to use inside of a . + /// A radio button is a variation of a that causes all other radio buttons in the same to be deselected upon selection. + /// public class RadioButton : Checkbox { + /// + /// The group that this radio button has. + /// All other radio buttons in the same that have the same group will be deselected when this radio button is selected. + /// public string Group; + /// + /// Creates a new radio button with the given settings + /// + /// The radio button's anchor + /// The radio button's size + /// The label to display next to the radio button + /// If the radio button should be checked by default + /// The group that the radio button has 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 { }; } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture.SetFromStyle(style.RadioTexture); diff --git a/MLEM.Ui/Elements/ScrollBar.cs b/MLEM.Ui/Elements/ScrollBar.cs index 8a8a5a3..dc1027e 100644 --- a/MLEM.Ui/Elements/ScrollBar.cs +++ b/MLEM.Ui/Elements/ScrollBar.cs @@ -10,13 +10,34 @@ using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A scroll bar element to be used inside of a . + /// 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 or any of its siblings. + /// public class ScrollBar : Element { + /// + /// The background texture for this scroll bar + /// public StyleProp Background; + /// + /// The texture of this scroll bar's scroller indicator + /// public StyleProp ScrollerTexture; + /// + /// The 's offset from the calculated position of the scroller. Use this to pad the scroller. + /// public Vector2 ScrollerOffset; + /// + /// The scroller's width and height + /// public Vector2 ScrollerSize; private float maxValue; + /// + /// 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. + /// public float MaxValue { get => this.maxValue; set { @@ -31,6 +52,10 @@ namespace MLEM.Ui.Elements { } private float scrollAdded; private float currValue; + /// + /// The current value of the scroll bar. + /// This is between 0 and at all times. + /// public float CurrentValue { get => this.currValue - this.scrollAdded; set { @@ -43,21 +68,51 @@ namespace MLEM.Ui.Elements { } } } + /// + /// Whether this scroll bar is horizontal + /// public readonly bool Horizontal; + /// + /// The amount added or removed from per single movement of the scroll wheel + /// public float StepPerScroll = 1; + /// + /// An event that is called when changes + /// public ValueChanged OnValueChanged; + /// + /// An event that is called when this scroll bar is automatically hidden from a + /// public GenericCallback OnAutoHide; private bool isMouseHeld; private bool isDragging; private bool isTouchHeld; + /// + /// This field determines if this scroll bar should automatically be hidden from a if there aren't enough children to allow for scrolling. + /// public bool AutoHideWhenEmpty; + /// + /// Whether smooth scrolling should be enabled for this scroll bar. + /// Smooth scrolling causes the to change gradually rather than instantly when scrolling. + /// public bool SmoothScrolling; + /// + /// The factor with which happens. + /// public float SmoothScrollFactor = 0.75F; static ScrollBar() { InputHandler.EnableGestures(GestureType.HorizontalDrag, GestureType.VerticalDrag); } + /// + /// Creates a new scroll bar with the given settings + /// + /// The scroll bar's anchor + /// The scroll bar's size + /// + /// + /// 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; } + /// public override void Update(GameTime time) { base.Update(time); @@ -134,6 +190,7 @@ namespace MLEM.Ui.Elements { } } + /// 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); } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Background.SetFromStyle(style.ScrollBarBackground); this.ScrollerTexture.SetFromStyle(style.ScrollBarScrollerTexture); } + /// + /// A delegate method used for + /// + /// The element whose current value changed + /// The element's new public delegate void ValueChanged(Element element, float value); } diff --git a/MLEM.Ui/Elements/Slider.cs b/MLEM.Ui/Elements/Slider.cs index 8fb1f81..885bd2e 100644 --- a/MLEM.Ui/Elements/Slider.cs +++ b/MLEM.Ui/Elements/Slider.cs @@ -4,8 +4,19 @@ using Microsoft.Xna.Framework.Input; using MLEM.Misc; namespace MLEM.Ui.Elements { + /// + /// A slider element for use inside of a . + /// A slider is a horizontal whose value can additionally be controlled using the and . + /// public class Slider : ScrollBar { + /// + /// Creates a new slider with the given settings + /// + /// The slider's anchor + /// The slider's size + /// The size of the slider's scroller indicator + /// The slider's maximum value 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 { }; } + /// public override void Update(GameTime time) { base.Update(time); diff --git a/MLEM.Ui/Elements/SpriteAnimationImage.cs b/MLEM.Ui/Elements/SpriteAnimationImage.cs index ed847fc..2ac0ad4 100644 --- a/MLEM.Ui/Elements/SpriteAnimationImage.cs +++ b/MLEM.Ui/Elements/SpriteAnimationImage.cs @@ -3,19 +3,41 @@ using MLEM.Animations; using MLEM.Textures; namespace MLEM.Ui.Elements { + /// + /// A sprite animation image for use inside of a . + /// A sprite animation image is an that displays a or . + /// public class SpriteAnimationImage : Image { + /// + /// The sprite animation group that is displayed by this image + /// public SpriteAnimationGroup Group; - public SpriteAnimationImage(Anchor anchor, Vector2 size, TextureRegion texture, SpriteAnimationGroup group, bool scaleToImage = false) : - base(anchor, size, texture, scaleToImage) { + /// + /// Creates a new sprite animation image with the given settings + /// + /// The image's anchor + /// The image's size + /// The sprite animation group to display + /// Whether this image element should scale to the texture + 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) { + /// + /// Creates a new sprite animation image with the given settings + /// + /// The image's anchor + /// The image's size + /// The sprite group to display + /// Whether this image element should scale to the texture + public SpriteAnimationImage(Anchor anchor, Vector2 size, SpriteAnimation animation, bool scaleToImage = false) : + this(anchor, size, new SpriteAnimationGroup().Add(animation, () => true), scaleToImage) { } + /// public override void Update(GameTime time) { base.Update(time); this.Group.Update(time); @@ -23,5 +45,4 @@ namespace MLEM.Ui.Elements { } } - } \ No newline at end of file diff --git a/MLEM.Ui/Elements/TextField.cs b/MLEM.Ui/Elements/TextField.cs index 2633fd0..4a0fd57 100644 --- a/MLEM.Ui/Elements/TextField.cs +++ b/MLEM.Ui/Elements/TextField.cs @@ -13,33 +13,100 @@ using MLEM.Ui.Style; using TextCopy; namespace MLEM.Ui.Elements { + /// + /// A text field element for use inside of a . + /// A text field is a selectable element that can be typed in, as well as copied and pasted from. + /// If is enabled, then this text field will automatically open an on-screen keyboard when pressed using . + /// public class TextField : Element { + /// + /// A that allows any visible character and spaces + /// public static readonly Rule DefaultRule = (field, add) => !add.Any(char.IsControl); + /// + /// A that only allows letters + /// public static readonly Rule OnlyLetters = (field, add) => add.All(char.IsLetter); + /// + /// A that only allows numerals + /// public static readonly Rule OnlyNumbers = (field, add) => add.All(char.IsNumber); + /// + /// A that only allows letters and numerals + /// public static readonly Rule LettersNumbers = (field, add) => add.All(c => char.IsLetter(c) || char.IsNumber(c)); + /// + /// The color that this text field's text should display with + /// public StyleProp TextColor; + /// + /// The color that the should display with + /// public StyleProp PlaceholderColor; + /// + /// This text field's texture + /// public StyleProp Texture; + /// + /// This text field's texture while it is hovered + /// public StyleProp HoveredTexture; + /// + /// The color that this text field should display with while it is hovered + /// public StyleProp HoveredColor; + /// + /// The scale that this text field should render text with + /// public StyleProp TextScale; + /// + /// The font that this text field should display text with + /// public StyleProp Font; private readonly StringBuilder text = new StringBuilder(); + /// + /// This text field's current text + /// public string Text => this.text.ToString(); + /// + /// The text that displays in this text field if is empty + /// public string PlaceholderText; + /// + /// An event that gets called when changes, either through input, or through a manual change. + /// public TextChanged OnTextChange; + /// + /// The x position that text should start rendering at, based on the x position of this text field. + /// public float TextOffsetX = 4; + /// + /// The width that the caret should render with. + /// public float CaretWidth = 0.5F; private double caretBlinkTimer; private string displayedText; private int textOffset; + /// + /// The rule used for text input. + /// Rules allow only certain characters to be allowed inside of a text field. + /// public Rule InputRule; + /// + /// The title of the field on mobile devices and consoles + /// public string MobileTitle; + /// + /// The description of the field on mobile devices and consoles + /// public string MobileDescription; private int caretPos; + /// + /// The position of the caret within the text. + /// This is always between 0 and the of + /// public int CaretPos { get { this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length); @@ -53,6 +120,13 @@ namespace MLEM.Ui.Elements { } } + /// + /// Creates a new text field with the given settings + /// + /// The text field's anchor + /// The text field's size + /// The text field's input rule + /// The font to use for drawing text 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); } + /// public override void Update(GameTime time) { base.Update(time); @@ -149,6 +224,7 @@ namespace MLEM.Ui.Elements { this.caretBlinkTimer = 0; } + /// 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); } + /// + /// Replaces this text field's text with the given text. + /// + /// The new text + /// If any characters that don't match the should be left out public void SetText(object text, bool removeMismatching = false) { if (removeMismatching) { var result = new StringBuilder(); @@ -191,6 +272,10 @@ namespace MLEM.Ui.Elements { this.HandleTextChange(); } + /// + /// Inserts the given text at the + /// + /// The text to insert public void InsertText(object text) { var strg = text.ToString(); if (!this.InputRule(this, strg)) @@ -200,6 +285,11 @@ namespace MLEM.Ui.Elements { this.HandleTextChange(); } + /// + /// Removes the given amount of text at the given index + /// + /// The index + /// The amount of text to remove 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(); } + /// 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); } + /// + /// A delegate method used for + /// + /// The text field whose text changed + /// The new text public delegate void TextChanged(TextField field, string text); + /// + /// A delegate method used for . + /// It should return whether the given text can be added to the text field. + /// + /// The text field + /// The text that is tried to be added public delegate bool Rule(TextField field, string textToAdd); } diff --git a/MLEM.Ui/Elements/Tooltip.cs b/MLEM.Ui/Elements/Tooltip.cs index f07fe2e..cc69e4f 100644 --- a/MLEM.Ui/Elements/Tooltip.cs +++ b/MLEM.Ui/Elements/Tooltip.cs @@ -5,11 +5,28 @@ using MLEM.Font; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { + /// + /// A tooltip element for use inside of a . + /// A tooltip is a 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. + /// public class Tooltip : Panel { + /// + /// The offset that this tooltip's top left corner should have from the mouse position + /// public StyleProp MouseOffset; + /// + /// The paragraph of text that this tooltip displays + /// public Paragraph Paragraph; + /// + /// Creates a new tooltip with the given settings + /// + /// The width of the tooltip + /// The text to display on the tooltip + /// The element that should automatically cause the tooltip to appear and disappear when hovered and not hovered, respectively 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 { } } + /// public override void Update(GameTime time) { base.Update(time); this.SnapPositionToMouse(); } + /// 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(); } + /// protected override void InitStyle(UiStyle style) { base.InitStyle(style); this.Texture.SetFromStyle(style.TooltipBackground); this.MouseOffset.SetFromStyle(style.TooltipOffset); } + /// + /// Causes this tooltip's position to be snapped to the mouse position. + /// public void SnapPositionToMouse() { var viewport = this.System.Viewport.Size; var offset = this.Input.MousePosition.ToVector2() / this.Scale + this.MouseOffset; diff --git a/MLEM.Ui/Elements/VerticalSpace.cs b/MLEM.Ui/Elements/VerticalSpace.cs index dcf004d..109ac32 100644 --- a/MLEM.Ui/Elements/VerticalSpace.cs +++ b/MLEM.Ui/Elements/VerticalSpace.cs @@ -1,8 +1,16 @@ using Microsoft.Xna.Framework; namespace MLEM.Ui.Elements { + /// + /// A vertical space element for use inside of a . + /// A vertical space is an invisible element that can be used to add vertical space between paragraphs or other elements. + /// public class VerticalSpace : Element { + /// + /// Creates a new vertical space with the given settings + /// + /// The height of the vertical space public VerticalSpace(int height) : base(Anchor.AutoCenter, new Vector2(1, height)) { this.CanBeSelected = false; } diff --git a/MLEM.Ui/Style/StyleProp.cs b/MLEM.Ui/Style/StyleProp.cs index 6d50092..8ccbff2 100644 --- a/MLEM.Ui/Style/StyleProp.cs +++ b/MLEM.Ui/Style/StyleProp.cs @@ -1,39 +1,82 @@ using System.Collections.Generic; +using MLEM.Ui.Elements; namespace MLEM.Ui.Style { + /// + /// A struct used by to store style properties. + /// This is a helper struct that allows default style settings from to be overridden by custom user settings easily. + /// Note that T implicitly converts to StyleProp{T} and vice versa. + /// + /// The type of style setting that this property stores public struct StyleProp { + /// + /// The currently applied style + /// public T Value { get; private set; } private bool isCustom; + /// + /// Creates a new style property with the given custom style. + /// + /// The custom style to apply public StyleProp(T value) { this.isCustom = true; this.Value = value; } + /// + /// Sets this style property's value and marks it as being set by a . + /// This allows this property to be overridden by custom style settings using . + /// + /// The style to apply public void SetFromStyle(T value) { if (!this.isCustom) { this.Value = value; } } + /// + /// Sets this style property's value and marks it as being custom. + /// This causes not to override the style value through a . + /// + /// public void Set(T value) { this.isCustom = true; this.Value = value; } + /// + /// Returns the current style value or, if is false, the given default value. + /// + /// The default to return if this style property has no value + /// The current value, or the default public T OrDefault(T def) { return this.HasValue() ? this.Value : def; } + /// + /// Returns whether this style property has a value assigned to it using or . + /// + /// Whether this style property has a value public bool HasValue() { return !EqualityComparer.Default.Equals(this.Value, default); } + /// + /// Implicitly converts a style property to its value. + /// + /// The property to convert + /// The style that the style property holds public static implicit operator T(StyleProp prop) { return prop.Value; } + /// + /// Implicitly converts a style to a style property. + /// + /// The property to convert + /// A style property with the given style value public static implicit operator StyleProp(T prop) { return new StyleProp(prop); } diff --git a/MLEM.Ui/Style/UiStyle.cs b/MLEM.Ui/Style/UiStyle.cs index eb2fef8..db3cbb2 100644 --- a/MLEM.Ui/Style/UiStyle.cs +++ b/MLEM.Ui/Style/UiStyle.cs @@ -5,37 +5,129 @@ using MLEM.Font; using MLEM.Formatting; using MLEM.Misc; using MLEM.Textures; +using MLEM.Ui.Elements; namespace MLEM.Ui.Style { + /// + /// The style settings for a . + /// Each 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 , meaning additional styles for custom components can easily be added using + /// public class UiStyle : GenericDataHolder { + /// + /// The texture that is rendered on top of the + /// public NinePatch SelectionIndicator; + /// + /// The texture that the element uses + /// public NinePatch ButtonTexture; + /// + /// The texture that the element uses when it is moused over () + /// Note that, if you just want to change the button's color when hovered, use . + /// public NinePatch ButtonHoveredTexture; + /// + /// The color that the element renders with when it is moused over () + /// public Color ButtonHoveredColor; + /// + /// The texture that the element uses when it + /// public NinePatch ButtonDisabledTexture; + /// + /// The color that the element uses when it + /// public Color ButtonDisabledColor; + /// + /// The texture that the element uses + /// public NinePatch PanelTexture; + /// + /// The texture that the element uses + /// public NinePatch TextFieldTexture; + /// + /// The texture that the element uses when it is moused over () + /// public NinePatch TextFieldHoveredTexture; + /// + /// The color that the renders with when it is moused over () + /// public Color TextFieldHoveredColor; + /// + /// The background texture that the element uses + /// public NinePatch ScrollBarBackground; + /// + /// The texture that the scroll indicator of the element uses + /// public NinePatch ScrollBarScrollerTexture; + /// + /// The texture that the element uses + /// public NinePatch CheckboxTexture; + /// + /// The texture that the element uses when it is moused over () + /// public NinePatch CheckboxHoveredTexture; + /// + /// The color that the element renders with when it is moused over () + /// public Color CheckboxHoveredColor; + /// + /// The texture that the element renders on top of its regular texture when it is + /// public TextureRegion CheckboxCheckmark; + /// + /// The texture that the element uses + /// public NinePatch RadioTexture; + /// + /// The texture that the element uses when it is moused over () + /// public NinePatch RadioHoveredTexture; + /// + /// The color that the element renders with when it is moused over () + /// public Color RadioHoveredColor; + /// + /// The texture that the renders on top of its regular texture when it is + /// public TextureRegion RadioCheckmark; + /// + /// The texture that the uses for its background + /// public NinePatch TooltipBackground; + /// + /// The offset of the element's top left corner from the mouse position + /// public Vector2 TooltipOffset; + /// + /// The texture that the element uses for its background + /// public NinePatch ProgressBarTexture; + /// + /// The color that the element renders with + /// public Color ProgressBarColor; + /// + /// The padding that the uses for its progress texture () + /// public Vector2 ProgressBarProgressPadding; + /// + /// The texture that the uses for displaying its progress + /// public NinePatch ProgressBarProgressTexture; + /// + /// The color that the renders its progress texture with + /// public Color ProgressBarProgressColor; + /// + /// The font that and other elements should use for rendering. + /// Note that, to specify a bold and italic font for , you should use and . + /// 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; + /// + /// The scale that text should be rendered with in and other elements + /// public float TextScale = 1; + /// + /// The that should be played when an element's and events are called. + /// Note that this sound is only played if the callbacks have any subscribers. + /// public SoundEffect ActionSound; } diff --git a/MLEM.Ui/Style/UntexturedStyle.cs b/MLEM.Ui/Style/UntexturedStyle.cs index 22989c4..e689cfc 100644 --- a/MLEM.Ui/Style/UntexturedStyle.cs +++ b/MLEM.Ui/Style/UntexturedStyle.cs @@ -7,8 +7,16 @@ using MLEM.Font; using MLEM.Textures; namespace MLEM.Ui.Style { + /// + /// The default, untextured . + /// 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 . + /// public class UntexturedStyle : UiStyle { + /// + /// Creates a new untextured style with textures generated by the given sprite batch + /// + /// The sprite batch to generate the textures with public UntexturedStyle(SpriteBatch batch) { this.SelectionIndicator = batch.GenerateTexture(Color.Transparent, Color.Red); this.ButtonTexture = batch.GenerateTexture(Color.CadetBlue); diff --git a/MLEM.Ui/UiControls.cs b/MLEM.Ui/UiControls.cs index 5cfdc3b..7fe791f 100644 --- a/MLEM.Ui/UiControls.cs +++ b/MLEM.Ui/UiControls.cs @@ -8,33 +8,119 @@ using MLEM.Extensions; using MLEM.Input; using MLEM.Misc; using MLEM.Ui.Elements; +using MLEM.Ui.Style; namespace MLEM.Ui { + /// + /// UiControls holds and manages all of the controls for a . + /// UiControls supports keyboard, mouse, gamepad and touch input using an underlying . + /// public class UiControls { + /// + /// The input handler that is used for querying input + /// public readonly InputHandler Input; + /// + /// This value ist true if the was created by this ui controls instance, or if it was passed in. + /// If the input handler was created by this instance, its method should be called by us. + /// protected readonly bool IsInputOurs; + /// + /// The that this ui controls instance is controlling + /// protected readonly UiSystem System; + /// + /// The that is currently active. + /// The active root element is the one with the highest that whose property is true. + /// public RootElement ActiveRoot { get; private set; } + /// + /// The that the mouse is currently over. + /// public Element MousedElement { get; private set; } private readonly Dictionary selectedElements = new Dictionary(); + /// + /// The element that is currently selected. + /// This is the of the . + /// public Element SelectedElement => this.GetSelectedElement(this.ActiveRoot); + /// + /// A list of , and/or that act as the buttons on the keyboard which perform the action. + /// If the is held, these buttons perform . + /// To easily add more elements to this list, use . + /// public object[] KeyboardButtons = {Keys.Space, Keys.Enter}; + /// + /// A list of , and/or that act as the buttons on a gamepad that perform the action. + /// To easily add more elements to this list, use . + /// public object[] GamepadButtons = {Buttons.A}; + /// + /// A list of , and/or that act as the buttons on a gamepad that perform the action. + /// To easily add more elements to this list, use . + /// public object[] SecondaryGamepadButtons = {Buttons.X}; + /// + /// A list of A list of , and/or that act as the buttons that select a that is above the currently selected element. + /// To easily add more elements to this list, use . + /// public object[] UpButtons = {Buttons.DPadUp, Buttons.LeftThumbstickUp}; + /// + /// A list of A list of , and/or that act as the buttons that select a that is below the currently selected element. + /// To easily add more elements to this list, use . + /// public object[] DownButtons = {Buttons.DPadDown, Buttons.LeftThumbstickDown}; + /// + /// A list of A list of , and/or that act as the buttons that select a that is to the left of the currently selected element. + /// To easily add more elements to this list, use . + /// public object[] LeftButtons = {Buttons.DPadLeft, Buttons.LeftThumbstickLeft}; + /// + /// A list of A list of , and/or that act as the buttons that select a that is to the right of the currently selected element. + /// To easily add more elements to this list, use . + /// public object[] RightButtons = {Buttons.DPadRight, Buttons.LeftThumbstickRight}; + /// + /// The zero-based index of the used for gamepad input. + /// If this index is lower than 0, every connected gamepad will trigger input. + /// public int GamepadIndex = -1; + /// + /// Set this to false to disable mouse input for these ui controls. + /// Note that this does not disable mouse input for the underlying . + /// public bool HandleMouse = true; + /// + /// Set this to false to disable keyboard input for these ui controls. + /// Note that this does not disable keyboard input for the underlying . + /// public bool HandleKeyboard = true; + /// + /// Set this to false to disable touch input for these ui controls. + /// Note that this does not disable touch input for the underlying . + /// public bool HandleTouch = true; + /// + /// Set this to false to disable gamepad input for these ui controls. + /// Note that this does not disable gamepad input for the underlying . + /// public bool HandleGamepad = true; - public bool IsAutoNavMode; + /// + /// If this value is true, the ui controls are in automatic navigation mode. + /// This means that the will be drawn around the . + /// To set this value, use or + /// + public bool IsAutoNavMode { get; internal set; } + /// + /// Creates a new instance of the ui controls. + /// You should rarely have to invoke this manually, since the handles it. + /// + /// The ui system to control with these controls + /// The input handler to use for controlling, or null to create a new one. 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); } + /// + /// Update this ui controls instance, causing the underlying to be updated, as well as ui input to be queried. + /// public virtual void Update() { if (this.IsInputOurs) this.Input.Update(); @@ -134,6 +223,13 @@ namespace MLEM.Ui { } } + /// + /// Returns the in the underlying that is currently below the given position. + /// Throughout the ui system, this is used for mouse input querying. + /// + /// The position to query + /// If this value is true, the will be applied. + /// The element under the position, or null if there isn't one 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; } + /// + /// Selects the given element that is a child of the given root element. + /// Optionally, automatic navigation can be forced on, causing the to be drawn around the element. + /// A simpler version of this method is . + /// + /// The root element of the + /// The element to select, or null to deselect the selected element. + /// Whether automatic navigation should be forced on 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; } + /// + /// Returns the selected element for the given root element. + /// A property equivalent to this method is . + /// + /// The root element whose selected element to return + /// The given root's selected element, or null if the root doesn't exist, or if there is no selected element for that root. public Element GetSelectedElement(RootElement root) { if (root == null) return null; @@ -172,6 +282,12 @@ namespace MLEM.Ui { return element; } + /// + /// Returns the next element to select when pressing the key during keyboard navigation. + /// If the backward boolean is true, the previous element should be returned instead. + /// + /// If we're going backwards (if is held) + /// The next or previous element to select protected virtual Element GetTabNextElement(bool backward) { if (this.ActiveRoot == null) return null; @@ -200,6 +316,11 @@ namespace MLEM.Ui { } } + /// + /// Returns the next element that should be selected during gamepad navigation, based on the that we're looking for elements in. + /// + /// The area that we're looking for next elements in + /// The first element found in that area protected virtual Element GetGamepadNextElement(RectangleF searchArea) { if (this.ActiveRoot == null) return null; @@ -256,6 +377,11 @@ namespace MLEM.Ui { this.SelectElement(this.ActiveRoot, next); } + /// + /// A helper function to add , or to an array of controls. + /// + /// The controls to add to + /// The additional controls to add to the controls list public static void AddButtons(ref object[] controls, params object[] additional) { controls = controls.Concat(additional).ToArray(); } diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs index 3fe17c4..0de7170 100644 --- a/MLEM.Ui/UiSystem.cs +++ b/MLEM.Ui/UiSystem.cs @@ -16,16 +16,42 @@ using MLEM.Ui.Elements; using MLEM.Ui.Style; namespace MLEM.Ui { + /// + /// A ui system is the central location for the updating and rendering of all ui s. + /// Each element added to the root of the ui system is assigned a that has additional data like a transformation matrix. + /// For more information on how ui systems work, check out + /// public class UiSystem : GameComponent { + /// + /// The graphics device that this ui system uses for its size calculations + /// public readonly GraphicsDevice GraphicsDevice; + /// + /// The game window that this ui system renders within + /// public readonly GameWindow Window; private readonly List rootElements = new List(); + /// + /// The viewport that this ui system is rendering inside of. + /// This is automatically updated during + /// public Rectangle Viewport { get; private set; } + /// + /// 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, is used as the size that uses default + /// public bool AutoScaleWithScreen; + /// + /// If is true, this is used as the screen size that uses the default + /// public Point AutoScaleReferenceSize; private float globalScale = 1; + /// + /// The global rendering scale of this ui system and all of its child elements. + /// If is true, this scale will be different based on the window size. + /// public float GlobalScale { get { if (!this.AutoScaleWithScreen) @@ -40,6 +66,10 @@ namespace MLEM.Ui { } private UiStyle style; + /// + /// The style options that this ui system and all of its elements use. + /// To set the default, untextured style, use . + /// public UiStyle Style { get => this.style; set { @@ -50,27 +80,94 @@ namespace MLEM.Ui { } } } + /// + /// The transparency (alpha value) that this ui system and all of its elements draw at. + /// public float DrawAlpha = 1; + /// + /// The blend state that this ui system and all of its elements draw with + /// public BlendState BlendState; + /// + /// The sampler state that this ui system and all of its elements draw with. + /// The default is , as that is the one that works best with pixel graphics. + /// public SamplerState SamplerState = SamplerState.PointClamp; + /// + /// The that this ui system's elements format their text with. + /// To add new formatting codes to the ui system, add them to this formatter. + /// public TextFormatter TextFormatter; + /// + /// The that this ui system is controlled by. + /// The ui controls are also the place to change bindings for controller and keyboard input. + /// public UiControls Controls; + /// + /// Event that is invoked after an is drawn, but before its children are drawn. + /// public Element.DrawCallback OnElementDrawn = (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha); + /// + /// Event that is invoked after the for each root element is drawn, but before its children are drawn. + /// public Element.DrawCallback OnSelectedElementDrawn; + /// + /// Event that is invoked when an is updated + /// public Element.TimeCallback OnElementUpdated = (e, time) => e.OnUpdated?.Invoke(e, time); + /// + /// Event that is invoked when an is pressed with the primary action key + /// public Element.GenericCallback OnElementPressed = e => e.OnPressed?.Invoke(e); + /// + /// Event that is invoked when an is pressed with the secondary action key + /// public Element.GenericCallback OnElementSecondaryPressed = e => e.OnSecondaryPressed?.Invoke(e); + /// + /// Event that is invoked when an is newly selected using automatic navigation, or after it has been pressed with the mouse. + /// public Element.GenericCallback OnElementSelected = e => e.OnSelected?.Invoke(e); + /// + /// Event that is invoked when an is deselected during the selection of a new element. + /// public Element.GenericCallback OnElementDeselected = e => e.OnDeselected?.Invoke(e); + /// + /// Event that is invoked when the mouse enters an + /// public Element.GenericCallback OnElementMouseEnter = e => e.OnMouseEnter?.Invoke(e); + /// + /// Event that is invoked when the mouse exits an + /// public Element.GenericCallback OnElementMouseExit = e => e.OnMouseExit?.Invoke(e); + /// + /// Event that is invoked when an 's display area changes + /// public Element.GenericCallback OnElementAreaUpdated = e => e.OnAreaUpdated?.Invoke(e); + /// + /// Event that is invoked when the that the mouse is currently over changes + /// public Element.GenericCallback OnMousedElementChanged; + /// + /// Event that is invoked when the selected changes, either through automatic navigation, or by pressing on an element with the mouse + /// public Element.GenericCallback OnSelectedElementChanged; + /// + /// Event that is invoked when a new is added to this ui system + /// public RootCallback OnRootAdded; + /// + /// Event that is invoked when a is removed from this ui system + /// public RootCallback OnRootRemoved; + /// + /// Creates a new ui system with the given settings. + /// + /// The game's window + /// The graphics device that should be used for viewport calculations + /// The style settings that this ui should have. Use for the default, untextured style. + /// The input handler that this ui's should use. If none is supplied, a new input handler is created for this ui. 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("]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F, t => this.Controls.MousedElement is Paragraph.Link link && link.Token == t)); } + /// + /// Update this ui system, querying the necessary events and updating each element's data. + /// + /// The game's time public override void Update(GameTime time) { this.Controls.Update(); @@ -114,6 +215,12 @@ namespace MLEM.Ui { } } + /// + /// Draws any and other elements that draw onto rather than directly onto the screen. + /// For drawing in this manner to work correctly, this method has to be called before your is cleared, and before everything else in your game is drawn. + /// + /// The game's time + /// The sprite batch to use for drawing public void DrawEarly(GameTime time, SpriteBatch batch) { foreach (var root in this.rootElements) { if (!root.Element.IsHidden) @@ -121,6 +228,12 @@ namespace MLEM.Ui { } } + /// + /// Draws any s onto the screen. + /// Note that, when using s with a scroll bar, needs to be called as well. + /// + /// The game's time + /// The sprite batch to use for drawing public void Draw(GameTime time, SpriteBatch batch) { foreach (var root in this.rootElements) { if (root.Element.IsHidden) @@ -132,6 +245,13 @@ namespace MLEM.Ui { } } + /// + /// Adds a new root element to this ui system and returns the newly created . + /// Note that, when adding new elements that should be part of the same ui (like buttons on a panel), should be used. + /// + /// The name of the new root element + /// The root element to add + /// The newly created root element, or null if an element with the specified name already exists. public RootElement Add(string name, Element element) { if (this.IndexOf(name) >= 0) return null; @@ -148,6 +268,10 @@ namespace MLEM.Ui { return root; } + /// + /// Removes the with the specified name, or does nothing if there is no such element. + /// + /// The name of the root element to remove public void Remove(string name) { var root = this.Get(name); if (root == null) @@ -163,6 +287,11 @@ namespace MLEM.Ui { this.OnRootRemoved?.Invoke(root); } + /// + /// Finds the with the given name. + /// + /// The root element's name + /// The root element with the given name, or null if no such element exists 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); } + /// + /// Returns an enumerable of all of the instances that this ui system holds. + /// + /// All of this ui system's root elements public IEnumerable GetRootElements() { for (var i = this.rootElements.Count - 1; i >= 0; i--) yield return this.rootElements[i]; } + /// + /// Applies the given action to all instances in this ui system recursively. + /// Note that, when this method is invoked, all root elements and all of their children receive the . + /// + /// The action to execute on each element public void ApplyToAll(Action action) { foreach (var root in this.rootElements) root.Element.AndChildren(action); } + /// + /// A delegate used for callbacks that involve a + /// + /// The root element public delegate void RootCallback(RootElement root); } + /// + /// A root element is a wrapper around an that contains additional data. + /// Root elements are only used for the element in each element tree that doesn't have a + /// To create a new root element, use + /// public class RootElement { + /// + /// The name of this root element + /// public readonly string Name; + /// + /// The element that this root element represents. + /// This is the only element in its family tree that does not have a . + /// public readonly Element Element; + /// + /// The that this root element is a part of. + /// public readonly UiSystem System; private float scale = 1; + /// + /// The scale of this root element. + /// Note that, to change the scale of every root element, you can use + /// public float Scale { get => this.scale; set { @@ -209,6 +370,10 @@ namespace MLEM.Ui { } } private int priority; + /// + /// The priority of this root element. + /// A higher priority means the element will be updated first, as well as rendered on top. + /// public int Priority { get => this.priority; set { @@ -216,18 +381,43 @@ namespace MLEM.Ui { this.System.SortRoots(); } } + /// + /// The actual scale of this root element. + /// This is a combination of this root element's as well as the ui system's . + /// public float ActualScale => this.System.GlobalScale * this.Scale; + /// + /// The transformation that this root element (and all of its children) has. + /// This transform is applied both to input, as well as to rendering. + /// public Matrix Transform = Matrix.Identity; + /// + /// An inversion of + /// public Matrix InvTransform => Matrix.Invert(this.Transform); + /// + /// The child element of this root element that is currently selected. + /// If there is no selected element in this root, this value will be null. + /// public Element SelectedElement => this.System.Controls.GetSelectedElement(this); + /// + /// Determines whether this root element contains any children that . + /// This value is automatically calculated. + /// public bool CanSelectContent { get; private set; } + /// + /// Event that is invoked when a is added to this root element or any of its children. + /// public Element.GenericCallback OnElementAdded; + /// + /// Even that is invoked when a is removed rom this root element of any of its children. + /// 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 { }; } + /// + /// Selects the given element that is a child of this root element. + /// Optionally, automatic navigation can be forced on, causing the to be drawn around the element. + /// + /// The element to select, or null to deselect the selected element. + /// Whether automatic navigation should be forced on public void SelectElement(Element element, bool? autoNav = null) { this.System.Controls.SelectElement(this, element, autoNav); }