2019-08-09 18:26:28 +02:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
2019-08-28 18:27:17 +02:00
|
|
|
using System.Linq;
|
2020-05-17 00:10:29 +02:00
|
|
|
using System.Text.RegularExpressions;
|
2019-08-09 18:26:28 +02:00
|
|
|
using Microsoft.Xna.Framework;
|
|
|
|
using Microsoft.Xna.Framework.Graphics;
|
2019-08-30 18:15:50 +02:00
|
|
|
using Microsoft.Xna.Framework.Input;
|
2019-08-09 18:26:28 +02:00
|
|
|
using MLEM.Extensions;
|
2019-08-09 19:28:48 +02:00
|
|
|
using MLEM.Font;
|
2020-05-17 00:10:29 +02:00
|
|
|
using MLEM.Formatting;
|
|
|
|
using MLEM.Formatting.Codes;
|
2019-08-09 22:04:26 +02:00
|
|
|
using MLEM.Input;
|
2019-09-01 18:34:19 +02:00
|
|
|
using MLEM.Misc;
|
2019-08-31 18:07:43 +02:00
|
|
|
using MLEM.Textures;
|
2019-08-09 18:26:28 +02:00
|
|
|
using MLEM.Ui.Elements;
|
2019-08-10 21:37:10 +02:00
|
|
|
using MLEM.Ui.Style;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
|
|
|
namespace MLEM.Ui {
|
2019-12-05 17:52:25 +01:00
|
|
|
public class UiSystem : GameComponent {
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-10 18:41:56 +02:00
|
|
|
public readonly GraphicsDevice GraphicsDevice;
|
2019-09-01 19:33:33 +02:00
|
|
|
public readonly GameWindow Window;
|
2019-08-09 18:26:28 +02:00
|
|
|
private readonly List<RootElement> rootElements = new List<RootElement>();
|
|
|
|
|
2020-02-06 01:59:33 +01:00
|
|
|
public Rectangle Viewport { get; private set; }
|
2019-08-23 19:46:36 +02:00
|
|
|
public bool AutoScaleWithScreen;
|
|
|
|
public Point AutoScaleReferenceSize;
|
2019-08-11 18:02:21 +02:00
|
|
|
private float globalScale = 1;
|
2019-08-09 23:43:50 +02:00
|
|
|
public float GlobalScale {
|
2019-08-23 19:46:36 +02:00
|
|
|
get {
|
|
|
|
if (!this.AutoScaleWithScreen)
|
|
|
|
return this.globalScale;
|
|
|
|
return Math.Min(this.Viewport.Width / (float) this.AutoScaleReferenceSize.X, this.Viewport.Height / (float) this.AutoScaleReferenceSize.Y) * this.globalScale;
|
|
|
|
}
|
2019-08-09 23:43:50 +02:00
|
|
|
set {
|
|
|
|
this.globalScale = value;
|
|
|
|
foreach (var root in this.rootElements)
|
|
|
|
root.Element.ForceUpdateArea();
|
|
|
|
}
|
|
|
|
}
|
2020-02-24 14:03:53 +01:00
|
|
|
|
2019-08-10 21:37:10 +02:00
|
|
|
private UiStyle style;
|
|
|
|
public UiStyle Style {
|
|
|
|
get => this.style;
|
|
|
|
set {
|
|
|
|
this.style = value;
|
|
|
|
foreach (var root in this.rootElements) {
|
2020-02-06 01:51:41 +01:00
|
|
|
root.Element.AndChildren(e => e.System = this);
|
2020-05-20 21:22:02 +02:00
|
|
|
root.Element.ForceUpdateArea();
|
2019-08-10 21:37:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-10 13:42:18 +02:00
|
|
|
public float DrawAlpha = 1;
|
2019-08-09 23:43:50 +02:00
|
|
|
public BlendState BlendState;
|
|
|
|
public SamplerState SamplerState = SamplerState.PointClamp;
|
2020-05-17 00:10:29 +02:00
|
|
|
public TextFormatter TextFormatter;
|
2019-08-28 18:27:17 +02:00
|
|
|
public UiControls Controls;
|
2019-08-30 18:15:50 +02:00
|
|
|
|
2019-09-25 16:47:27 +02:00
|
|
|
public Element.DrawCallback OnElementDrawn = (e, time, batch, alpha) => e.OnDrawn?.Invoke(e, time, batch, alpha);
|
2019-08-31 18:07:43 +02:00
|
|
|
public Element.DrawCallback OnSelectedElementDrawn;
|
2019-09-25 16:47:27 +02:00
|
|
|
public Element.TimeCallback OnElementUpdated = (e, time) => e.OnUpdated?.Invoke(e, time);
|
2019-09-13 13:57:25 +02:00
|
|
|
public Element.GenericCallback OnElementPressed = e => e.OnPressed?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementSecondaryPressed = e => e.OnSecondaryPressed?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementSelected = e => e.OnSelected?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementDeselected = e => e.OnDeselected?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementMouseEnter = e => e.OnMouseEnter?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementMouseExit = e => e.OnMouseExit?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnElementAreaUpdated = e => e.OnAreaUpdated?.Invoke(e);
|
|
|
|
public Element.GenericCallback OnMousedElementChanged;
|
|
|
|
public Element.GenericCallback OnSelectedElementChanged;
|
2019-12-05 14:53:13 +01:00
|
|
|
public RootCallback OnRootAdded;
|
|
|
|
public RootCallback OnRootRemoved;
|
2019-08-31 18:07:43 +02:00
|
|
|
|
2020-02-01 21:16:10 +01:00
|
|
|
public UiSystem(GameWindow window, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) : base(null) {
|
2019-08-28 18:27:17 +02:00
|
|
|
this.Controls = new UiControls(this, inputHandler);
|
2019-08-10 18:41:56 +02:00
|
|
|
this.GraphicsDevice = device;
|
2020-02-01 21:16:10 +01:00
|
|
|
this.Window = window;
|
2019-08-10 21:37:10 +02:00
|
|
|
this.style = style;
|
2020-04-19 14:04:44 +02:00
|
|
|
this.Viewport = new Rectangle(Point.Zero, window.ClientBounds.Size);
|
2019-08-23 19:46:36 +02:00
|
|
|
this.AutoScaleReferenceSize = this.Viewport.Size;
|
2019-08-09 19:28:48 +02:00
|
|
|
|
2020-02-01 21:16:10 +01:00
|
|
|
window.ClientSizeChanged += (sender, args) => {
|
2020-04-19 14:04:44 +02:00
|
|
|
this.Viewport = new Rectangle(Point.Zero, window.ClientBounds.Size);
|
2019-08-09 18:26:28 +02:00
|
|
|
foreach (var root in this.rootElements)
|
|
|
|
root.Element.ForceUpdateArea();
|
|
|
|
};
|
2019-08-30 19:05:27 +02:00
|
|
|
|
2020-02-24 14:03:53 +01:00
|
|
|
TextInputWrapper.Current.AddListener(window, (sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
|
2019-09-13 13:57:25 +02:00
|
|
|
this.OnMousedElementChanged = e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
|
|
|
|
this.OnSelectedElementChanged = e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
|
2019-09-04 17:19:31 +02:00
|
|
|
this.OnSelectedElementDrawn = (element, time, batch, alpha) => {
|
2020-02-06 17:36:51 +01:00
|
|
|
if (this.Controls.IsAutoNavMode && element.SelectionIndicator.HasValue()) {
|
2019-09-08 23:55:56 +02:00
|
|
|
batch.Draw(element.SelectionIndicator, element.DisplayArea, Color.White * alpha, element.Scale / 2);
|
2019-08-31 18:07:43 +02:00
|
|
|
}
|
|
|
|
};
|
2020-01-14 22:39:40 +01:00
|
|
|
this.OnElementPressed += e => {
|
|
|
|
if (e.OnPressed != null)
|
|
|
|
e.ActionSound.Value?.Replay();
|
|
|
|
};
|
|
|
|
this.OnElementSecondaryPressed += e => {
|
|
|
|
if (e.OnSecondaryPressed != null)
|
|
|
|
e.SecondActionSound.Value?.Replay();
|
|
|
|
};
|
2020-05-17 00:10:29 +02:00
|
|
|
|
|
|
|
this.TextFormatter = new TextFormatter();
|
2020-05-20 01:02:18 +02:00
|
|
|
this.TextFormatter.Codes.Add(new Regex("<l(?: ([^>]+))?>"), (f, m, r) => new LinkCode(m, r, 1 / 16F, 0.85F, t => this.Controls.MousedElement is Paragraph.Link link && link.Token == t));
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-12-05 17:52:25 +01:00
|
|
|
public override void Update(GameTime time) {
|
2019-08-28 18:27:17 +02:00
|
|
|
this.Controls.Update();
|
2019-08-10 18:41:56 +02:00
|
|
|
|
2019-12-05 14:59:53 +01:00
|
|
|
for (var i = this.rootElements.Count - 1; i >= 0; i--) {
|
|
|
|
this.rootElements[i].Element.Update(time);
|
|
|
|
}
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-15 14:59:15 +02:00
|
|
|
public void DrawEarly(GameTime time, SpriteBatch batch) {
|
2019-08-12 19:44:16 +02:00
|
|
|
foreach (var root in this.rootElements) {
|
|
|
|
if (!root.Element.IsHidden)
|
2019-09-02 19:55:26 +02:00
|
|
|
root.Element.DrawEarly(time, batch, this.DrawAlpha * root.Element.DrawAlpha, this.BlendState, this.SamplerState, root.Transform);
|
2019-08-12 19:44:16 +02:00
|
|
|
}
|
2019-08-15 14:59:15 +02:00
|
|
|
}
|
2019-08-12 19:44:16 +02:00
|
|
|
|
2019-08-15 14:59:15 +02:00
|
|
|
public void Draw(GameTime time, SpriteBatch batch) {
|
2019-08-09 19:39:51 +02:00
|
|
|
foreach (var root in this.rootElements) {
|
2019-08-11 18:02:21 +02:00
|
|
|
if (root.Element.IsHidden)
|
|
|
|
continue;
|
2019-09-02 19:55:26 +02:00
|
|
|
batch.Begin(SpriteSortMode.Deferred, this.BlendState, this.SamplerState, null, null, null, root.Transform);
|
2019-09-20 13:22:05 +02:00
|
|
|
var alpha = this.DrawAlpha * root.Element.DrawAlpha;
|
|
|
|
root.Element.Draw(time, batch, alpha, this.BlendState, this.SamplerState, root.Transform);
|
2019-08-11 18:02:21 +02:00
|
|
|
batch.End();
|
2019-08-09 19:39:51 +02:00
|
|
|
}
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-23 22:23:10 +02:00
|
|
|
public RootElement Add(string name, Element element) {
|
2019-12-25 12:15:55 +01:00
|
|
|
if (this.IndexOf(name) >= 0)
|
|
|
|
return null;
|
2019-08-23 22:23:10 +02:00
|
|
|
var root = new RootElement(name, element, this);
|
2019-12-25 12:15:55 +01:00
|
|
|
this.rootElements.Add(root);
|
2019-09-02 18:41:05 +02:00
|
|
|
root.Element.AndChildren(e => {
|
2019-08-28 18:58:05 +02:00
|
|
|
e.Root = root;
|
2020-02-06 01:51:41 +01:00
|
|
|
e.System = this;
|
2019-12-08 21:49:15 +01:00
|
|
|
root.OnElementAdded(e);
|
2019-11-18 02:20:09 +01:00
|
|
|
e.SetAreaDirty();
|
2019-08-28 18:58:05 +02:00
|
|
|
});
|
2019-12-05 14:53:13 +01:00
|
|
|
this.OnRootAdded?.Invoke(root);
|
2019-12-25 12:15:55 +01:00
|
|
|
this.SortRoots();
|
|
|
|
return root;
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void Remove(string name) {
|
2019-08-23 22:23:10 +02:00
|
|
|
var root = this.Get(name);
|
|
|
|
if (root == null)
|
2019-08-09 18:26:28 +02:00
|
|
|
return;
|
2019-08-23 22:23:10 +02:00
|
|
|
this.rootElements.Remove(root);
|
2020-03-17 15:04:36 +01:00
|
|
|
this.Controls.SelectElement(root, null);
|
2019-09-02 18:41:05 +02:00
|
|
|
root.Element.AndChildren(e => {
|
2019-08-28 18:58:05 +02:00
|
|
|
e.Root = null;
|
2020-02-06 01:51:41 +01:00
|
|
|
e.System = null;
|
2019-12-08 21:49:15 +01:00
|
|
|
root.OnElementRemoved(e);
|
2019-11-18 02:20:09 +01:00
|
|
|
e.SetAreaDirty();
|
2019-08-28 18:58:05 +02:00
|
|
|
});
|
2019-12-05 14:53:13 +01:00
|
|
|
this.OnRootRemoved?.Invoke(root);
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-11 18:02:21 +02:00
|
|
|
public RootElement Get(string name) {
|
2019-08-09 18:26:28 +02:00
|
|
|
var index = this.IndexOf(name);
|
2019-08-11 18:02:21 +02:00
|
|
|
return index < 0 ? null : this.rootElements[index];
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private int IndexOf(string name) {
|
|
|
|
return this.rootElements.FindIndex(element => element.Name == name);
|
|
|
|
}
|
|
|
|
|
2019-12-25 12:15:55 +01:00
|
|
|
internal void SortRoots() {
|
|
|
|
// Normal list sorting isn't stable, but ordering is
|
|
|
|
var sorted = this.rootElements.OrderBy(root => root.Priority).ToArray();
|
|
|
|
this.rootElements.Clear();
|
|
|
|
this.rootElements.AddRange(sorted);
|
|
|
|
}
|
|
|
|
|
2019-08-28 18:27:17 +02:00
|
|
|
public IEnumerable<RootElement> GetRootElements() {
|
|
|
|
for (var i = this.rootElements.Count - 1; i >= 0; i--)
|
|
|
|
yield return this.rootElements[i];
|
2019-08-28 18:58:05 +02:00
|
|
|
}
|
|
|
|
|
2019-09-02 18:41:05 +02:00
|
|
|
public void ApplyToAll(Action<Element> action) {
|
2019-08-28 18:58:05 +02:00
|
|
|
foreach (var root in this.rootElements)
|
2019-09-02 18:41:05 +02:00
|
|
|
root.Element.AndChildren(action);
|
2019-08-09 22:04:26 +02:00
|
|
|
}
|
|
|
|
|
2019-12-05 14:53:13 +01:00
|
|
|
public delegate void RootCallback(RootElement root);
|
|
|
|
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-11 18:02:21 +02:00
|
|
|
public class RootElement {
|
2019-08-09 18:26:28 +02:00
|
|
|
|
|
|
|
public readonly string Name;
|
|
|
|
public readonly Element Element;
|
2020-02-06 01:51:41 +01:00
|
|
|
public readonly UiSystem System;
|
2019-08-11 18:02:21 +02:00
|
|
|
private float scale = 1;
|
|
|
|
public float Scale {
|
|
|
|
get => this.scale;
|
|
|
|
set {
|
|
|
|
if (this.scale == value)
|
|
|
|
return;
|
|
|
|
this.scale = value;
|
|
|
|
this.Element.ForceUpdateArea();
|
|
|
|
}
|
|
|
|
}
|
2019-12-25 12:15:55 +01:00
|
|
|
private int priority;
|
|
|
|
public int Priority {
|
|
|
|
get => this.priority;
|
|
|
|
set {
|
|
|
|
this.priority = value;
|
2020-02-06 01:51:41 +01:00
|
|
|
this.System.SortRoots();
|
2019-12-25 12:15:55 +01:00
|
|
|
}
|
|
|
|
}
|
2020-02-06 01:51:41 +01:00
|
|
|
public float ActualScale => this.System.GlobalScale * this.Scale;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-09-02 19:55:26 +02:00
|
|
|
public Matrix Transform = Matrix.Identity;
|
|
|
|
public Matrix InvTransform => Matrix.Invert(this.Transform);
|
|
|
|
|
2020-03-17 15:04:36 +01:00
|
|
|
public Element SelectedElement => this.System.Controls.GetSelectedElement(this);
|
2019-12-08 21:49:15 +01:00
|
|
|
public bool CanSelectContent { get; private set; }
|
|
|
|
|
|
|
|
public Element.GenericCallback OnElementAdded;
|
|
|
|
public Element.GenericCallback OnElementRemoved;
|
2019-09-09 17:12:36 +02:00
|
|
|
|
2020-02-06 01:51:41 +01:00
|
|
|
public RootElement(string name, Element element, UiSystem system) {
|
2019-08-09 18:26:28 +02:00
|
|
|
this.Name = name;
|
|
|
|
this.Element = element;
|
2020-02-06 01:51:41 +01:00
|
|
|
this.System = system;
|
2019-12-08 21:49:15 +01:00
|
|
|
|
|
|
|
this.OnElementAdded += e => {
|
|
|
|
if (e.CanBeSelected)
|
|
|
|
this.CanSelectContent = true;
|
|
|
|
};
|
|
|
|
this.OnElementRemoved += e => {
|
|
|
|
if (e.CanBeSelected && !this.Element.GetChildren(regardGrandchildren: true).Any(c => c.CanBeSelected))
|
|
|
|
this.CanSelectContent = false;
|
|
|
|
};
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2020-03-16 15:33:25 +01:00
|
|
|
public void SelectElement(Element element, bool? autoNav = null) {
|
2020-03-17 15:04:36 +01:00
|
|
|
this.System.Controls.SelectElement(this, element, autoNav);
|
2019-09-09 17:12:36 +02:00
|
|
|
}
|
|
|
|
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
}
|