using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
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 https://mlem.ellpeck.de/articles/ui.html.
///
public class UiSystem : GameComponent {
///
/// The viewport that this ui system is rendering inside of.
/// This is automatically updated during by default.
///
public Rectangle Viewport {
get => this.viewport;
set {
this.viewport = value;
foreach (var root in this.rootElements)
root.Element.ForceUpdateArea();
}
}
///
/// 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;
///
/// 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)
return this.globalScale;
return Math.Min(this.Viewport.Width / (float) this.AutoScaleReferenceSize.X, this.Viewport.Height / (float) this.AutoScaleReferenceSize.Y) * this.globalScale;
}
set {
this.globalScale = value;
foreach (var root in this.rootElements)
root.Element.ForceUpdateArea();
}
}
///
/// 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 {
this.style = value;
foreach (var root in this.rootElements)
root.Element.AndChildren(e => e.Style = e.Style.OrStyle(value));
}
}
///
/// The transparency (alpha value) that this ui system and all of its elements draw at.
///
public float DrawAlpha = 1;
///
/// The spriteb atch context that this ui system and all of its elements should draw with.
/// The default is , as that is the one that works best with pixel graphics.
///
public SpriteBatchContext SpriteBatchContext = new SpriteBatchContext(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;
///
/// The update and rendering statistics to be used for runtime debugging and profiling.
/// The metrics are reset accordingly every frame: is called at the start of , and is called at the start of .
///
public UiMetrics Metrics;
///
/// Event that is invoked after an is drawn, but before its children are drawn.
///
public event Element.DrawCallback OnElementDrawn;
///
/// Event that is invoked after the for each root element is drawn, but before its children are drawn.
///
public event Element.DrawCallback OnSelectedElementDrawn;
///
/// Event that is invoked when an is updated
///
public event Element.TimeCallback OnElementUpdated;
///
/// Event that is invoked when an is pressed with the primary action key
///
public event Element.GenericCallback OnElementPressed;
///
/// Event that is invoked when an is pressed with the secondary action key
///
public event Element.GenericCallback OnElementSecondaryPressed;
///
/// Event that is invoked when an is newly selected using automatic navigation, or after it has been pressed with the mouse.
///
public event Element.GenericCallback OnElementSelected;
///
/// Event that is invoked when an is deselected during the selection of a new element.
///
public event Element.GenericCallback OnElementDeselected;
///
/// Event that is invoked when the mouse enters an
///
public event Element.GenericCallback OnElementMouseEnter;
///
/// Event that is invoked when the mouse exits an
///
public event Element.GenericCallback OnElementMouseExit;
///
/// Event that is invoked when an starts being touched
///
public event Element.GenericCallback OnElementTouchEnter;
///
/// Event that is invoked when an stops being touched
///
public event Element.GenericCallback OnElementTouchExit;
///
/// Event that is invoked when an 's display area changes
///
public event Element.GenericCallback OnElementAreaUpdated;
///
/// Event that is called when an 's method is called while setting its .
///
public event Element.GenericCallback OnElementStyleInit;
///
/// Event that is invoked when the that the mouse is currently over changes
///
public event Element.GenericCallback OnMousedElementChanged;
///
/// Event that is invoked when the that is being touched changes
///
public event Element.GenericCallback OnTouchedElementChanged;
///
/// Event that is invoked when the selected changes, either through automatic navigation, or by pressing on an element with the mouse
///
public event Element.GenericCallback OnSelectedElementChanged;
///
/// Event that is invoked when a new is added to this ui system
///
public event RootCallback OnRootAdded;
///
/// Event that is invoked when a is removed from this ui system
///
public event RootCallback OnRootRemoved;
private readonly List rootElements = new List();
private readonly Stopwatch stopwatch = new Stopwatch();
private float globalScale = 1;
private bool drewEarly;
private UiStyle style;
private Rectangle viewport;
///
/// Creates a new ui system with the given settings.
///
/// The game
/// 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.
/// If this value is set to true, the ui system's will be set automatically based on the 's size. Defaults to true.
/// Whether default font modifier codes should be added to this ui system's , including bold, italic, strikethrough, shadow, subscript, and more.
/// Whether default color codes should be added to this ui system's , including all values and the ability to use custom colors.
/// Whether default animation codes should be added to this ui system's , namely the wobbly animation.
/// Whether default macros should be added to this ui system's , including TeX's ~ non-breaking space and more.
/// Whether -based formatting codes should be added to this ui system's , including codes and font switching.
public UiSystem(Game game, UiStyle style, InputHandler inputHandler = null, bool automaticViewport = true, bool hasFontModifierFormatting = true, bool hasColorFormatting = true, bool hasAnimationFormatting = true, bool hasMacroFormatting = true, bool hasUiFormatting = true) : base(game) {
this.Controls = new UiControls(this, inputHandler);
this.style = style;
this.OnElementDrawn += (e, time, batch, alpha, context) => e.OnDrawn?.Invoke(e, time, batch, alpha, context);
this.OnElementUpdated += (e, time) => e.OnUpdated?.Invoke(e, time);
this.OnElementPressed += e => e.OnPressed?.Invoke(e);
this.OnElementSecondaryPressed += e => e.OnSecondaryPressed?.Invoke(e);
this.OnElementSelected += e => e.OnSelected?.Invoke(e);
this.OnElementDeselected += e => e.OnDeselected?.Invoke(e);
this.OnElementMouseEnter += e => e.OnMouseEnter?.Invoke(e);
this.OnElementMouseExit += e => e.OnMouseExit?.Invoke(e);
this.OnElementTouchEnter += e => e.OnTouchEnter?.Invoke(e);
this.OnElementTouchExit += e => e.OnTouchExit?.Invoke(e);
this.OnElementAreaUpdated += e => e.OnAreaUpdated?.Invoke(e);
this.OnElementStyleInit += e => e.OnStyleInit?.Invoke(e);
this.OnMousedElementChanged += e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
this.OnTouchedElementChanged += e => this.ApplyToAll(t => t.OnTouchedElementChanged?.Invoke(t, e));
this.OnSelectedElementChanged += e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
this.OnSelectedElementDrawn += (element, time, batch, alpha, context) => {
if (this.Controls.IsAutoNavMode && element.SelectionIndicator.HasValue())
batch.Draw(element.SelectionIndicator, element.DisplayArea, Color.White * alpha, element.Scale / 2);
};
this.OnElementPressed += e => {
if (e.OnPressed != null)
e.ActionSound.Value?.Play();
};
this.OnElementSecondaryPressed += e => {
if (e.OnSecondaryPressed != null)
e.SecondActionSound.Value?.Play();
};
MlemPlatform.Current?.AddTextInputListener(game.Window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
if (automaticViewport) {
this.Viewport = new Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height);
this.AutoScaleReferenceSize = new Point(this.Viewport.Width, this.Viewport.Height);
game.Window.ClientSizeChanged += (sender, args) => {
this.Viewport = new Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height);
};
}
this.TextFormatter = new TextFormatter(hasFontModifierFormatting, hasColorFormatting, hasAnimationFormatting, hasMacroFormatting);
if (hasUiFormatting) {
this.TextFormatter.Codes.Add(new Regex("]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F,
t => this.Controls.MousedElement is Paragraph.Link l1 && l1.Token == t || this.Controls.TouchedElement is Paragraph.Link l2 && l2.Token == t,
d => this.Style.LinkColor));
this.TextFormatter.Codes.Add(new Regex("]+)>"), (_, m, r) => new FontCode(m, r,
f => this.Style.AdditionalFonts != null && this.Style.AdditionalFonts.TryGetValue(m.Groups[1].Value, out var c) ? c : f));
}
}
///
/// 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.Metrics.ResetUpdates();
this.stopwatch.Restart();
this.Controls.Update();
for (var i = this.rootElements.Count - 1; i >= 0; i--)
this.rootElements[i].Element.Update(time);
this.stopwatch.Stop();
this.Metrics.UpdateTime += this.stopwatch.Elapsed;
}
///
/// Draws any s onto the screen.
///
/// The game's time
/// The sprite batch to use for drawing
public void Draw(GameTime time, SpriteBatch batch) {
if (!this.drewEarly)
this.Metrics.ResetDraws();
this.stopwatch.Restart();
foreach (var root in this.rootElements) {
if (root.Element.IsHidden)
continue;
var context = this.SpriteBatchContext;
context.TransformMatrix = root.Transform * context.TransformMatrix;
batch.Begin(context);
root.Element.DrawTransformed(time, batch, this.DrawAlpha * root.Element.DrawAlpha, context);
batch.End();
}
this.stopwatch.Stop();
this.Metrics.DrawTime += this.stopwatch.Elapsed;
this.drewEarly = false;
}
///
/// 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;
var root = new RootElement(name, element, this);
this.rootElements.Add(root);
root.Element.AndChildren(e => {
e.AddedToUi(this, root);
e.SetAreaDirty();
});
this.OnRootAdded?.Invoke(root);
root.InvokeOnAddedToUi(this);
this.SortRoots();
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)
return;
this.rootElements.Remove(root);
this.Controls.SelectElement(root, null);
root.Element.AndChildren(e => {
e.RemovedFromUi();
e.SetAreaDirty();
});
this.OnRootRemoved?.Invoke(root);
root.InvokeOnRemovedFromUi(this);
}
///
/// 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];
}
private int IndexOf(string name) {
return this.rootElements.FindIndex(element => element.Name == name);
}
internal void SortRoots() {
// Normal list sorting isn't stable, but ordering is
var sorted = this.rootElements.OrderBy(root => root.Priority).ToArray();
this.rootElements.Clear();
this.rootElements.AddRange(sorted);
}
///
/// 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);
}
///
protected override void Dispose(bool disposing) {
if (disposing) {
while (this.rootElements.Count > 0)
this.Remove(this.rootElements[0].Name);
}
}
internal void InvokeOnElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
this.OnElementDrawn?.Invoke(element, time, batch, alpha, context);
}
internal void InvokeOnSelectedElementDrawn(Element element, GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
this.OnSelectedElementDrawn?.Invoke(element, time, batch, alpha, context);
}
internal void InvokeOnElementUpdated(Element element, GameTime time) {
this.OnElementUpdated?.Invoke(element, time);
}
internal void InvokeOnElementAreaUpdated(Element element) {
this.OnElementAreaUpdated?.Invoke(element);
}
internal void InvokeOnElementStyleInit(Element element) {
this.OnElementStyleInit?.Invoke(element);
}
internal void InvokeOnElementPressed(Element element) {
this.OnElementPressed?.Invoke(element);
}
internal void InvokeOnElementSecondaryPressed(Element element) {
this.OnElementSecondaryPressed?.Invoke(element);
}
internal void InvokeOnElementSelected(Element element) {
this.OnElementSelected?.Invoke(element);
}
internal void InvokeOnElementDeselected(Element element) {
this.OnElementDeselected?.Invoke(element);
}
internal void InvokeOnSelectedElementChanged(Element element) {
this.OnSelectedElementChanged?.Invoke(element);
}
internal void InvokeOnElementMouseExit(Element element) {
this.OnElementMouseExit?.Invoke(element);
}
internal void InvokeOnElementMouseEnter(Element element) {
this.OnElementMouseEnter?.Invoke(element);
}
internal void InvokeOnMousedElementChanged(Element element) {
this.OnMousedElementChanged?.Invoke(element);
}
internal void InvokeOnElementTouchExit(Element element) {
this.OnElementTouchExit?.Invoke(element);
}
internal void InvokeOnElementTouchEnter(Element element) {
this.OnElementTouchEnter?.Invoke(element);
}
internal void InvokeOnTouchedElementChanged(Element element) {
this.OnTouchedElementChanged?.Invoke(element);
}
///
/// 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 : GenericDataHolder {
///
/// 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;
///
/// 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 {
if (this.scale == value)
return;
this.scale = value;
this.Element.ForceUpdateArea();
}
}
///
/// 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 {
this.priority = value;
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, and used in .
///
public bool CanSelectContent => this.Element.CanBeSelected || this.Element.GetChildren(c => c.CanBeSelected, true).Any();
///
/// Determines whether this root element can become the .
/// This property returns if is and the is not hidden, or if it has been set to manually.
///
public bool CanBeActive {
get => this.canBeActive || !this.Element.IsHidden && this.CanSelectContent;
set => this.canBeActive = value;
}
///
/// Event that is invoked when a is added to this root element or any of its children.
///
public event Element.GenericCallback OnElementAdded;
///
/// Event that is invoked when a is removed rom this root element or any of its children.
///
public event Element.GenericCallback OnElementRemoved;
///
/// Event that is invoked when this gets added to a in
///
public event Action OnAddedToUi;
///
/// Event that is invoked when this gets removed from a in
///
public event Action OnRemovedFromUi;
private float scale = 1;
private bool canBeActive;
private int priority;
internal RootElement(string name, Element element, UiSystem system) {
this.Name = name;
this.Element = element;
this.System = system;
}
///
/// 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. If this is , the automatic navigation state will stay the same.
/// An optional to set. If this is , the navigation type will stay the same.
public void SelectElement(Element element, bool? autoNav = null, UiControls.NavigationType? navType = null) {
this.System.Controls.SelectElement(this, element, autoNav, navType);
}
///
/// Scales this root element's matrix based on the given scale and origin.
///
/// The scale to use
/// The origin to use for scaling, or null to use this root's element's center point
public void ScaleOrigin(float scale, Vector2? origin = null) {
this.Transform = Matrix.CreateScale(scale, scale, 1) * Matrix.CreateTranslation(new Vector3((1 - scale) * (origin ?? this.Element.DisplayArea.Center), 0));
}
internal void InvokeOnElementAdded(Element element) {
this.OnElementAdded?.Invoke(element);
}
internal void InvokeOnElementRemoved(Element element) {
this.OnElementRemoved?.Invoke(element);
}
internal void InvokeOnAddedToUi(UiSystem system) {
this.OnAddedToUi?.Invoke(system);
}
internal void InvokeOnRemovedFromUi(UiSystem system) {
this.OnRemovedFromUi?.Invoke(system);
}
}
}