2019-08-09 18:26:28 +02:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using Microsoft.Xna.Framework;
|
|
|
|
using Microsoft.Xna.Framework.Graphics;
|
2019-08-10 18:41:56 +02:00
|
|
|
using Microsoft.Xna.Framework.Input;
|
2019-08-09 18:26:28 +02:00
|
|
|
using MLEM.Extensions;
|
2019-08-09 22:04:26 +02:00
|
|
|
using MLEM.Input;
|
2019-08-10 21:37:10 +02:00
|
|
|
using MLEM.Ui.Style;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
|
|
|
namespace MLEM.Ui.Elements {
|
2019-08-09 19:28:48 +02:00
|
|
|
public abstract class Element {
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-09 22:04:26 +02:00
|
|
|
private readonly List<Element> children = new List<Element>();
|
2019-08-09 18:26:28 +02:00
|
|
|
private Anchor anchor;
|
2019-08-09 19:28:48 +02:00
|
|
|
private Vector2 size;
|
2019-08-09 18:26:28 +02:00
|
|
|
private Point offset;
|
|
|
|
private Point padding;
|
2019-08-09 19:28:48 +02:00
|
|
|
private Point childPadding;
|
2019-08-09 18:26:28 +02:00
|
|
|
public Anchor Anchor {
|
|
|
|
get => this.anchor;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.anchor == value)
|
|
|
|
return;
|
2019-08-09 18:26:28 +02:00
|
|
|
this.anchor = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
2019-08-09 19:28:48 +02:00
|
|
|
public Vector2 Size {
|
2019-08-09 18:26:28 +02:00
|
|
|
get => this.size;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.size == value)
|
|
|
|
return;
|
2019-08-09 18:26:28 +02:00
|
|
|
this.size = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public Point PositionOffset {
|
|
|
|
get => this.offset;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.offset == value)
|
|
|
|
return;
|
2019-08-09 18:26:28 +02:00
|
|
|
this.offset = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public Point Padding {
|
|
|
|
get => this.padding;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.padding == value)
|
|
|
|
return;
|
2019-08-09 18:26:28 +02:00
|
|
|
this.padding = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
2019-08-09 19:28:48 +02:00
|
|
|
public Point ChildPadding {
|
|
|
|
get => this.childPadding;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.childPadding == value)
|
|
|
|
return;
|
2019-08-09 19:28:48 +02:00
|
|
|
this.childPadding = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-09 22:04:26 +02:00
|
|
|
public MouseClickCallback OnClicked;
|
2019-08-10 18:41:56 +02:00
|
|
|
public GenericCallback OnSelected;
|
|
|
|
public GenericCallback OnDeselected;
|
2019-08-09 22:04:26 +02:00
|
|
|
public MouseCallback OnMouseEnter;
|
|
|
|
public MouseCallback OnMouseExit;
|
2019-08-10 18:41:56 +02:00
|
|
|
public TextInputCallback OnTextInput;
|
2019-08-09 22:04:26 +02:00
|
|
|
|
2019-08-10 21:37:10 +02:00
|
|
|
private UiSystem system;
|
|
|
|
public UiSystem System {
|
|
|
|
get => this.system;
|
|
|
|
set {
|
|
|
|
this.system = value;
|
|
|
|
if (this.system != null && !this.HasCustomStyle)
|
|
|
|
this.InitStyle(this.system.Style);
|
|
|
|
}
|
|
|
|
}
|
2019-08-10 18:41:56 +02:00
|
|
|
protected InputHandler Input => this.System.InputHandler;
|
2019-08-09 18:26:28 +02:00
|
|
|
public Element Parent { get; private set; }
|
2019-08-09 22:04:26 +02:00
|
|
|
public bool IsMouseOver { get; private set; }
|
2019-08-10 18:41:56 +02:00
|
|
|
public bool IsSelected { get; private set; }
|
2019-08-09 22:23:16 +02:00
|
|
|
private bool isHidden;
|
|
|
|
public bool IsHidden {
|
|
|
|
get => this.isHidden;
|
|
|
|
set {
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.isHidden == value)
|
|
|
|
return;
|
2019-08-09 22:23:16 +02:00
|
|
|
this.isHidden = value;
|
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
}
|
2019-08-09 22:04:26 +02:00
|
|
|
public bool IgnoresMouse;
|
2019-08-10 13:42:18 +02:00
|
|
|
public float DrawAlpha = 1;
|
2019-08-10 21:37:10 +02:00
|
|
|
public bool HasCustomStyle;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
|
|
|
private Rectangle area;
|
|
|
|
public Rectangle Area {
|
|
|
|
get {
|
|
|
|
this.UpdateAreaIfDirty();
|
|
|
|
return this.area;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public Rectangle DisplayArea {
|
|
|
|
get {
|
|
|
|
var padded = this.Area;
|
|
|
|
padded.Location += this.Padding;
|
|
|
|
padded.Width -= this.Padding.X * 2;
|
|
|
|
padded.Height -= this.Padding.Y * 2;
|
|
|
|
return padded;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2019-08-09 19:28:48 +02:00
|
|
|
private bool areaDirty;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-09 22:04:26 +02:00
|
|
|
public Element(Anchor anchor, Vector2 size) {
|
2019-08-09 18:26:28 +02:00
|
|
|
this.anchor = anchor;
|
|
|
|
this.size = size;
|
2019-08-09 22:04:26 +02:00
|
|
|
|
|
|
|
this.OnMouseEnter += (element, mousePos) => this.IsMouseOver = true;
|
|
|
|
this.OnMouseExit += (element, mousePos) => this.IsMouseOver = false;
|
2019-08-10 18:41:56 +02:00
|
|
|
this.OnSelected += element => this.IsSelected = true;
|
|
|
|
this.OnDeselected += element => this.IsSelected = false;
|
2019-08-09 22:04:26 +02:00
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
this.SetDirty();
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-10 18:41:56 +02:00
|
|
|
public T AddChild<T>(T element, int index = -1) where T : Element {
|
2019-08-09 22:23:16 +02:00
|
|
|
if (index < 0 || index > this.children.Count)
|
|
|
|
index = this.children.Count;
|
|
|
|
this.children.Insert(index, element);
|
2019-08-09 18:26:28 +02:00
|
|
|
element.Parent = this;
|
2019-08-10 21:37:10 +02:00
|
|
|
element.PropagateUiSystem(this.System);
|
2019-08-09 18:26:28 +02:00
|
|
|
this.SetDirty();
|
2019-08-09 22:23:16 +02:00
|
|
|
return element;
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void RemoveChild(Element element) {
|
|
|
|
this.children.Remove(element);
|
|
|
|
element.Parent = null;
|
2019-08-10 21:39:35 +02:00
|
|
|
element.PropagateUiSystem(null);
|
2019-08-09 18:26:28 +02:00
|
|
|
this.SetDirty();
|
|
|
|
}
|
|
|
|
|
2019-08-09 22:23:16 +02:00
|
|
|
public void MoveToFront() {
|
|
|
|
if (this.Parent != null) {
|
|
|
|
this.Parent.RemoveChild(this);
|
|
|
|
this.Parent.AddChild(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void MoveToBack() {
|
|
|
|
if (this.Parent != null) {
|
|
|
|
this.Parent.RemoveChild(this);
|
|
|
|
this.Parent.AddChild(this, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-09 18:26:28 +02:00
|
|
|
public void SetDirty() {
|
2019-08-09 19:28:48 +02:00
|
|
|
this.areaDirty = true;
|
2019-08-10 15:12:27 +02:00
|
|
|
if (this.Anchor >= Anchor.AutoLeft && this.Parent != null)
|
2019-08-09 22:23:16 +02:00
|
|
|
this.Parent.SetDirty();
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void UpdateAreaIfDirty() {
|
2019-08-09 19:28:48 +02:00
|
|
|
if (this.areaDirty)
|
2019-08-09 18:26:28 +02:00
|
|
|
this.ForceUpdateArea();
|
|
|
|
}
|
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
public virtual void ForceUpdateArea() {
|
|
|
|
this.areaDirty = false;
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
Rectangle parentArea;
|
|
|
|
if (this.Parent != null) {
|
|
|
|
parentArea = this.Parent.area;
|
|
|
|
parentArea.Location += this.Parent.ChildPadding;
|
|
|
|
parentArea.Width -= this.Parent.ChildPadding.X * 2;
|
|
|
|
parentArea.Height -= this.Parent.ChildPadding.Y * 2;
|
|
|
|
} else {
|
|
|
|
parentArea = this.System.ScaledViewport;
|
|
|
|
}
|
2019-08-09 18:26:28 +02:00
|
|
|
var parentCenterX = parentArea.X + parentArea.Width / 2;
|
|
|
|
var parentCenterY = parentArea.Y + parentArea.Height / 2;
|
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
var actualSize = this.CalcActualSize(parentArea);
|
2019-08-09 18:26:28 +02:00
|
|
|
var pos = new Point();
|
|
|
|
|
|
|
|
switch (this.anchor) {
|
|
|
|
case Anchor.TopLeft:
|
|
|
|
case Anchor.AutoLeft:
|
|
|
|
case Anchor.AutoInline:
|
|
|
|
case Anchor.AutoInlineIgnoreOverflow:
|
|
|
|
pos.X = parentArea.X + this.offset.X;
|
|
|
|
pos.Y = parentArea.Y + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.TopCenter:
|
|
|
|
case Anchor.AutoCenter:
|
|
|
|
pos.X = parentCenterX - actualSize.X / 2 + this.offset.X;
|
|
|
|
pos.Y = parentArea.Y + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.TopRight:
|
|
|
|
case Anchor.AutoRight:
|
|
|
|
pos.X = parentArea.Right - actualSize.X - this.offset.X;
|
|
|
|
pos.Y = parentArea.Y + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.CenterLeft:
|
|
|
|
pos.X = parentArea.X + this.offset.X;
|
|
|
|
pos.Y = parentCenterY - actualSize.Y / 2 + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.Center:
|
|
|
|
pos.X = parentCenterX - actualSize.X / 2 + this.offset.X;
|
|
|
|
pos.Y = parentCenterY - actualSize.Y / 2 + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.CenterRight:
|
|
|
|
pos.X = parentArea.Right - actualSize.X - this.offset.X;
|
|
|
|
pos.Y = parentCenterY - actualSize.Y / 2 + this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.BottomLeft:
|
|
|
|
pos.X = parentArea.X + this.offset.X;
|
|
|
|
pos.Y = parentArea.Bottom - actualSize.Y - this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.BottomCenter:
|
|
|
|
pos.X = parentCenterX - actualSize.X / 2 + this.offset.X;
|
|
|
|
pos.Y = parentArea.Bottom - actualSize.Y - this.offset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.BottomRight:
|
|
|
|
pos.X = parentArea.Right - actualSize.X - this.offset.X;
|
|
|
|
pos.Y = parentArea.Bottom - actualSize.Y - this.offset.Y;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.Anchor >= Anchor.AutoLeft) {
|
2019-08-09 22:23:16 +02:00
|
|
|
var previousChild = this.GetPreviousChild(false);
|
2019-08-09 18:26:28 +02:00
|
|
|
if (previousChild != null) {
|
|
|
|
var prevArea = previousChild.Area;
|
|
|
|
switch (this.Anchor) {
|
|
|
|
case Anchor.AutoLeft:
|
|
|
|
case Anchor.AutoCenter:
|
|
|
|
case Anchor.AutoRight:
|
|
|
|
pos.Y = prevArea.Bottom + this.PositionOffset.Y;
|
|
|
|
break;
|
|
|
|
case Anchor.AutoInline:
|
|
|
|
var newX = prevArea.Right + this.PositionOffset.X;
|
|
|
|
if (newX + actualSize.X <= parentArea.Right) {
|
|
|
|
pos.X = newX;
|
|
|
|
pos.Y = prevArea.Y;
|
|
|
|
} else {
|
|
|
|
pos.Y = prevArea.Bottom + this.PositionOffset.Y;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case Anchor.AutoInlineIgnoreOverflow:
|
|
|
|
pos.X = prevArea.Right + this.PositionOffset.X;
|
|
|
|
pos.Y = prevArea.Y;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.area = new Rectangle(pos, actualSize);
|
|
|
|
|
|
|
|
foreach (var child in this.children)
|
|
|
|
child.ForceUpdateArea();
|
|
|
|
}
|
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
protected virtual Point CalcActualSize(Rectangle parentArea) {
|
|
|
|
return new Point(
|
|
|
|
(this.size.X > 1 ? this.size.X : parentArea.Width * this.size.X).Floor(),
|
|
|
|
(this.size.Y > 1 ? this.size.Y : parentArea.Height * this.size.Y).Floor());
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
|
2019-08-09 22:23:16 +02:00
|
|
|
protected Element GetPreviousChild(bool hiddenAlso) {
|
2019-08-09 18:26:28 +02:00
|
|
|
if (this.Parent == null)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
Element lastChild = null;
|
|
|
|
foreach (var child in this.Parent.children) {
|
2019-08-09 22:23:16 +02:00
|
|
|
if (!hiddenAlso && child.IsHidden)
|
|
|
|
continue;
|
2019-08-09 18:26:28 +02:00
|
|
|
if (child == this)
|
|
|
|
break;
|
|
|
|
lastChild = child;
|
|
|
|
}
|
|
|
|
return lastChild;
|
|
|
|
}
|
|
|
|
|
2019-08-09 19:28:48 +02:00
|
|
|
public virtual void Update(GameTime time) {
|
2019-08-09 18:26:28 +02:00
|
|
|
foreach (var child in this.children)
|
|
|
|
child.Update(time);
|
|
|
|
}
|
|
|
|
|
2019-08-10 13:42:18 +02:00
|
|
|
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha) {
|
2019-08-09 19:39:51 +02:00
|
|
|
foreach (var child in this.children) {
|
|
|
|
if (!child.IsHidden)
|
2019-08-10 13:42:18 +02:00
|
|
|
child.Draw(time, batch, alpha * child.DrawAlpha);
|
2019-08-09 19:39:51 +02:00
|
|
|
}
|
2019-08-09 19:28:48 +02:00
|
|
|
}
|
|
|
|
|
2019-08-10 13:42:18 +02:00
|
|
|
public virtual void DrawUnbound(GameTime time, SpriteBatch batch, float alpha, BlendState blendState = null, SamplerState samplerState = null) {
|
2019-08-09 19:39:51 +02:00
|
|
|
foreach (var child in this.children) {
|
|
|
|
if (!child.IsHidden)
|
2019-08-10 13:42:18 +02:00
|
|
|
child.DrawUnbound(time, batch, alpha * child.DrawAlpha, blendState, samplerState);
|
2019-08-09 19:39:51 +02:00
|
|
|
}
|
2019-08-09 19:28:48 +02:00
|
|
|
}
|
2019-08-09 18:26:28 +02:00
|
|
|
|
2019-08-09 22:04:26 +02:00
|
|
|
public Element GetMousedElement(Vector2 mousePos) {
|
|
|
|
if (this.IsHidden || this.IgnoresMouse)
|
|
|
|
return null;
|
|
|
|
if (!this.Area.Contains(mousePos))
|
|
|
|
return null;
|
2019-08-09 22:23:16 +02:00
|
|
|
for (var i = this.children.Count - 1; i >= 0; i--) {
|
|
|
|
var element = this.children[i].GetMousedElement(mousePos);
|
2019-08-09 22:04:26 +02:00
|
|
|
if (element != null)
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2019-08-10 21:37:10 +02:00
|
|
|
protected virtual void InitStyle(UiStyle style) {
|
|
|
|
}
|
|
|
|
|
2019-08-09 22:04:26 +02:00
|
|
|
public delegate void MouseClickCallback(Element element, Vector2 mousePos, MouseButton button);
|
|
|
|
|
|
|
|
public delegate void MouseCallback(Element element, Vector2 mousePos);
|
|
|
|
|
2019-08-10 18:41:56 +02:00
|
|
|
public delegate void TextInputCallback(Element element, Keys key, char character);
|
|
|
|
|
|
|
|
public delegate void GenericCallback(Element element);
|
|
|
|
|
2019-08-10 21:37:10 +02:00
|
|
|
internal void PropagateUiSystem(UiSystem system) {
|
2019-08-10 18:41:56 +02:00
|
|
|
this.System = system;
|
|
|
|
foreach (var child in this.children)
|
2019-08-10 21:37:10 +02:00
|
|
|
child.PropagateUiSystem(system);
|
2019-08-10 18:41:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
internal void PropagateInput(Keys key, char character) {
|
|
|
|
this.OnTextInput?.Invoke(this, key, character);
|
|
|
|
foreach (var child in this.children)
|
|
|
|
child.PropagateInput(key, character);
|
|
|
|
}
|
|
|
|
|
2019-08-09 18:26:28 +02:00
|
|
|
}
|
|
|
|
}
|