1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-11-04 22:49:08 +01:00
MLEM/MLEM.Ui/Elements/Element.cs

476 lines
18 KiB
C#
Raw Normal View History

2019-08-09 18:26:28 +02:00
using System;
using System.Collections.Generic;
2019-08-12 14:44:42 +02:00
using System.Diagnostics;
2019-08-18 18:32:34 +02:00
using System.Linq;
2019-08-09 18:26:28 +02:00
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;
using MLEM.Input;
2019-08-28 18:27:17 +02:00
using MLEM.Textures;
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-12 14:44:42 +02:00
protected readonly List<Element> Children = new List<Element>();
private readonly List<Element> sortedChildren = new List<Element>();
protected List<Element> SortedChildren {
get {
this.UpdateSortedChildrenIfDirty();
return this.sortedChildren;
}
}
2019-08-09 18:26:28 +02:00
private Anchor anchor;
2019-08-09 19:28:48 +02:00
private Vector2 size;
2019-08-13 23:54:29 +02:00
private Vector2 offset;
2019-08-11 18:50:39 +02:00
public Point Padding;
public Point ScaledPadding => this.Padding.Multiply(this.Scale);
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;
2019-08-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 18:26:28 +02:00
}
}
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;
2019-08-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 18:26:28 +02:00
}
}
public Vector2 ScaledSize => this.size * this.Scale;
2019-08-13 23:54:29 +02:00
public Vector2 PositionOffset {
2019-08-09 18:26:28 +02:00
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;
2019-08-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 18:26:28 +02:00
}
}
2019-08-13 23:54:29 +02:00
public Point ScaledOffset => (this.offset * this.Scale).ToPoint();
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;
2019-08-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 19:28:48 +02:00
}
}
public Rectangle ChildPaddedArea {
get {
2019-08-24 22:27:47 +02:00
var padded = this.UnscrolledArea;
padded.Location += this.ScaledChildPadding;
padded.Width -= this.ScaledChildPadding.X * 2;
padded.Height -= this.ScaledChildPadding.Y * 2;
return padded;
}
}
public Point ScaledChildPadding => this.childPadding.Multiply(this.Scale);
2019-08-20 21:35:53 +02:00
public Vector2 AddedDisplayOffset;
public Point ScaledDisplayOffset => (this.AddedDisplayOffset * this.Scale).ToPoint();
public Vector2 AddedDisplayScale;
public Point ScaledDisplayScale => (this.AddedDisplayScale * this.Scale).ToPoint();
2019-08-09 18:26:28 +02:00
public GenericCallback OnPressed;
public GenericCallback OnSecondaryPressed;
2019-08-10 18:41:56 +02:00
public GenericCallback OnSelected;
public GenericCallback OnDeselected;
public GenericCallback OnMouseEnter;
public GenericCallback OnMouseExit;
2019-08-10 18:41:56 +02:00
public TextInputCallback OnTextInput;
public GenericCallback OnAreaUpdated;
2019-08-28 18:58:05 +02:00
public OtherElementCallback OnMousedElementChanged;
public OtherElementCallback OnSelectedElementChanged;
2019-08-10 21:37:10 +02:00
private UiSystem system;
public UiSystem System {
get => this.system;
2019-08-28 18:58:05 +02:00
internal set {
2019-08-10 21:37:10 +02:00
this.system = value;
if (this.system != null && !this.HasCustomStyle)
this.InitStyle(this.system.Style);
}
}
protected UiControls Controls => this.System.Controls;
2019-08-28 18:27:17 +02:00
protected InputHandler Input => this.Controls.Input;
2019-08-28 18:58:05 +02:00
public RootElement Root { get; internal set; }
public float Scale => this.Root.ActualScale;
2019-08-09 18:26:28 +02:00
public Element Parent { get; private set; }
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;
2019-08-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 22:23:16 +02:00
}
}
2019-08-28 18:27:17 +02:00
public bool CanBeSelected = true;
public bool CanBeMoused = true;
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-11 18:50:39 +02:00
public bool SetHeightBasedOnChildren;
public bool CanAutoAnchorsAttach = true;
2019-08-09 18:26:28 +02:00
private Rectangle area;
2019-08-24 22:27:47 +02:00
public Rectangle UnscrolledArea {
2019-08-09 18:26:28 +02:00
get {
this.UpdateAreaIfDirty();
return this.area;
}
}
2019-08-24 22:27:47 +02:00
public Rectangle Area => this.UnscrolledArea.OffsetCopy(this.ScaledScrollOffset);
public Point ScrollOffset;
public Point ScaledScrollOffset => this.ScrollOffset.Multiply(this.Scale);
2019-08-09 18:26:28 +02:00
public Rectangle DisplayArea {
get {
var padded = this.Area;
2019-08-20 21:35:53 +02:00
padded.Inflate(this.ScaledDisplayScale.X, this.ScaledDisplayScale.Y);
padded.Location += this.ScaledPadding + this.ScaledDisplayOffset;
padded.Width -= this.ScaledPadding.X * 2;
padded.Height -= this.ScaledPadding.Y * 2;
2019-08-09 18:26:28 +02:00
return padded;
}
}
2019-08-12 14:44:42 +02:00
private int priority;
public int Priority {
get => this.priority;
set {
this.priority = value;
if (this.Parent != null)
this.Parent.SetSortedChildrenDirty();
}
}
2019-08-09 19:28:48 +02:00
private bool areaDirty;
2019-08-12 14:44:42 +02:00
private bool sortedChildrenDirty;
2019-08-28 18:27:17 +02:00
public NinePatch SelectionIndicator;
2019-08-09 18:26:28 +02:00
public Element(Anchor anchor, Vector2 size) {
2019-08-09 18:26:28 +02:00
this.anchor = anchor;
this.size = size;
this.OnMouseEnter += element => this.IsMouseOver = true;
this.OnMouseExit += element => 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-12 14:44:42 +02:00
this.SetAreaDirty();
2019-08-09 18:26:28 +02:00
}
public T AddChild<T>(T element, int index = -1) where T : Element {
2019-08-12 14:44:42 +02:00
if (index < 0 || index > this.Children.Count)
index = this.Children.Count;
this.Children.Insert(index, element);
element.Parent = this;
2019-09-02 18:41:05 +02:00
element.AndChildren(e => {
2019-08-28 18:58:05 +02:00
e.Root = this.Root;
e.System = this.System;
});
2019-08-12 14:44:42 +02:00
this.SetSortedChildrenDirty();
this.SetAreaDirty();
2019-08-09 22:23:16 +02:00
return element;
2019-08-09 18:26:28 +02:00
}
public void RemoveChild(Element element) {
2019-08-12 14:44:42 +02:00
this.Children.Remove(element);
element.Parent = null;
2019-09-02 18:41:05 +02:00
element.AndChildren(e => {
2019-08-28 18:58:05 +02:00
e.Root = null;
e.System = null;
});
2019-08-12 14:44:42 +02:00
this.SetSortedChildrenDirty();
this.SetAreaDirty();
2019-08-09 18:26:28 +02:00
}
2019-08-12 14:44:42 +02:00
public void SetAreaDirty() {
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-12 14:44:42 +02:00
this.Parent.SetAreaDirty();
}
public void SetSortedChildrenDirty() {
this.sortedChildrenDirty = true;
}
public void UpdateSortedChildrenIfDirty() {
if (this.sortedChildrenDirty) {
this.sortedChildrenDirty = false;
this.sortedChildren.Clear();
this.sortedChildren.AddRange(this.Children);
this.sortedChildren.Sort((e1, e2) => e1.Priority.CompareTo(e2.Priority));
2019-08-12 14:44:42 +02:00
}
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;
if (this.IsHidden)
return;
2019-08-09 18:26:28 +02:00
var parentArea = this.Parent != null ? this.Parent.ChildPaddedArea : this.system.Viewport;
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.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.TopCenter:
case Anchor.AutoCenter:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.TopRight:
case Anchor.AutoRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Y + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.CenterLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.Center:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.CenterRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentCenterY - actualSize.Y / 2 + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.BottomLeft:
pos.X = parentArea.X + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.BottomCenter:
pos.X = parentCenterX - actualSize.X / 2 + this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.BottomRight:
pos.X = parentArea.Right - actualSize.X - this.ScaledOffset.X;
pos.Y = parentArea.Bottom - actualSize.Y - this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
}
if (this.Anchor >= Anchor.AutoLeft) {
Element previousChild;
if (this.Anchor == Anchor.AutoInline || this.Anchor == Anchor.AutoInlineIgnoreOverflow) {
previousChild = this.GetOlderSibling(false, false);
} else {
previousChild = this.GetLowestOlderSibling(false, false);
}
2019-08-09 18:26:28 +02:00
if (previousChild != null) {
var prevArea = previousChild.GetAreaForAutoAnchors();
2019-08-09 18:26:28 +02:00
switch (this.Anchor) {
case Anchor.AutoLeft:
case Anchor.AutoCenter:
case Anchor.AutoRight:
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
break;
case Anchor.AutoInline:
var newX = prevArea.Right + this.ScaledOffset.X;
2019-08-09 18:26:28 +02:00
if (newX + actualSize.X <= parentArea.Right) {
pos.X = newX;
pos.Y = prevArea.Y;
} else {
pos.Y = prevArea.Bottom + this.ScaledOffset.Y;
2019-08-09 18:26:28 +02:00
}
break;
case Anchor.AutoInlineIgnoreOverflow:
pos.X = prevArea.Right + this.ScaledOffset.X;
2019-08-09 18:26:28 +02:00
pos.Y = prevArea.Y;
break;
}
}
}
2019-09-04 17:23:23 +02:00
this.area = new Rectangle(pos, actualSize);
this.OnAreaUpdated?.Invoke(this);
2019-08-12 14:44:42 +02:00
foreach (var child in this.Children)
2019-08-09 18:26:28 +02:00
child.ForceUpdateArea();
2019-08-11 18:50:39 +02:00
2019-08-20 23:04:21 +02:00
if (this.SetHeightBasedOnChildren && this.Children.Count > 0) {
var lowest = this.GetLowestChild(false, true);
2019-08-24 22:27:47 +02:00
var newHeight = (lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Y) / this.Scale;
2019-08-11 18:50:39 +02:00
if (newHeight != this.size.Y) {
this.size.Y = newHeight;
if (this.Anchor > Anchor.TopRight)
this.ForceUpdateArea();
2019-08-11 18:50:39 +02:00
}
}
2019-08-09 18:26:28 +02:00
}
2019-08-09 19:28:48 +02:00
protected virtual Point CalcActualSize(Rectangle parentArea) {
return new Point(
(this.size.X > 1 ? this.ScaledSize.X : parentArea.Width * this.size.X).Floor(),
(this.size.Y > 1 ? this.ScaledSize.Y : parentArea.Height * this.size.Y).Floor());
2019-08-09 18:26:28 +02:00
}
2019-08-20 23:04:21 +02:00
protected virtual Rectangle GetAreaForAutoAnchors() {
2019-08-24 22:27:47 +02:00
return this.UnscrolledArea;
}
public Element GetLowestChild(bool hiddenAlso, bool unattachableAlso) {
Element lowest = null;
// the lowest child is expected to be towards the back, so search is usually faster if done backwards
for (var i = this.Children.Count - 1; i >= 0; i--) {
var child = this.Children[i];
if (!hiddenAlso && child.IsHidden || !unattachableAlso && !child.CanAutoAnchorsAttach)
continue;
if (child.Anchor > Anchor.TopRight && child.Anchor < Anchor.AutoLeft)
continue;
2019-08-24 22:27:47 +02:00
if (lowest == null || child.UnscrolledArea.Bottom > lowest.UnscrolledArea.Bottom)
lowest = child;
}
return lowest;
}
public Element GetLowestOlderSibling(bool hiddenAlso, bool unattachableAlso) {
2019-08-09 18:26:28 +02:00
if (this.Parent == null)
return null;
Element lowest = null;
2019-08-12 14:44:42 +02:00
foreach (var child in this.Parent.Children) {
2019-08-09 18:26:28 +02:00
if (child == this)
break;
if (!hiddenAlso && child.IsHidden || !unattachableAlso && !child.CanAutoAnchorsAttach)
continue;
2019-08-24 22:27:47 +02:00
if (lowest == null || child.UnscrolledArea.Bottom >= lowest.UnscrolledArea.Bottom)
lowest = child;
2019-08-09 18:26:28 +02:00
}
return lowest;
2019-08-09 18:26:28 +02:00
}
public Element GetOlderSibling(bool hiddenAlso, bool unattachableAlso) {
if (this.Parent == null)
return null;
Element older = null;
foreach (var child in this.Parent.Children) {
if (child == this)
break;
if (!hiddenAlso && child.IsHidden || !unattachableAlso && !child.CanAutoAnchorsAttach)
continue;
older = child;
}
return older;
}
2019-08-18 18:32:34 +02:00
public IEnumerable<Element> GetSiblings(bool hiddenAlso) {
if (this.Parent == null)
yield break;
foreach (var child in this.Parent.Children) {
if (!hiddenAlso && child.IsHidden)
continue;
if (child != this)
yield return child;
}
}
2019-08-18 18:32:34 +02:00
public IEnumerable<Element> GetChildren(Func<Element, bool> condition = null, bool regardChildrensChildren = false) {
foreach (var child in this.Children) {
if (regardChildrensChildren) {
foreach (var cc in child.GetChildren(condition, true))
yield return cc;
}
2019-08-20 21:35:53 +02:00
if (condition == null || condition(child))
yield return child;
2019-08-18 18:32:34 +02:00
}
}
public IEnumerable<T> GetChildren<T>(Func<T, bool> condition = null, bool regardChildrensChildren = false) where T : Element {
foreach (var child in this.Children) {
if (regardChildrensChildren) {
foreach (var cc in child.GetChildren(condition, true))
yield return cc;
}
2019-08-20 21:35:53 +02:00
if (child is T t && (condition == null || condition(t)))
yield return t;
2019-08-18 18:32:34 +02:00
}
}
2019-08-21 17:00:22 +02:00
public IEnumerable<Element> GetParentTree() {
if (this.Parent == null)
yield break;
yield return this.Parent;
foreach (var parent in this.Parent.GetParentTree())
yield return parent;
}
2019-08-09 19:28:48 +02:00
public virtual void Update(GameTime time) {
2019-08-12 14:44:42 +02:00
foreach (var child in this.SortedChildren)
2019-08-09 18:26:28 +02:00
child.Update(time);
}
public virtual void Draw(GameTime time, SpriteBatch batch, float alpha) {
this.System.OnElementDrawn?.Invoke(this, time, batch, alpha);
2019-08-12 14:44:42 +02:00
foreach (var child in this.SortedChildren) {
2019-08-09 19:39:51 +02:00
if (!child.IsHidden)
child.Draw(time, batch, alpha * child.DrawAlpha);
2019-08-09 19:39:51 +02:00
}
2019-08-28 18:27:17 +02:00
if (this.IsSelected)
this.System.OnSelectedElementDrawn?.Invoke(this, time, batch, alpha);
2019-08-09 19:28:48 +02:00
}
public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState, SamplerState samplerState, Matrix matrix) {
2019-08-12 14:44:42 +02:00
foreach (var child in this.SortedChildren) {
2019-08-09 19:39:51 +02:00
if (!child.IsHidden)
child.DrawEarly(time, batch, alpha * child.DrawAlpha, blendState, samplerState, matrix);
2019-08-09 19:39:51 +02:00
}
2019-08-09 19:28:48 +02:00
}
2019-08-09 18:26:28 +02:00
public virtual Element GetElementUnderPos(Point position) {
2019-08-28 18:27:17 +02:00
if (this.IsHidden)
return null;
2019-08-12 14:44:42 +02:00
for (var i = this.SortedChildren.Count - 1; i >= 0; i--) {
var element = this.SortedChildren[i].GetElementUnderPos(position);
if (element != null)
return element;
}
return this.CanBeMoused && this.Area.Contains(position) ? this : null;
}
2019-09-02 18:41:05 +02:00
public void AndChildren(Action<Element> action) {
action(this);
foreach (var child in this.Children)
child.AndChildren(action);
}
2019-08-10 21:37:10 +02:00
protected virtual void InitStyle(UiStyle style) {
2019-08-28 18:27:17 +02:00
this.SelectionIndicator = style.SelectionIndicator;
2019-08-10 21:37:10 +02:00
}
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-28 18:58:05 +02:00
public delegate void OtherElementCallback(Element thisElement, Element otherElement);
public delegate void DrawCallback(Element element, GameTime time, SpriteBatch batch, float alpha);
2019-08-09 18:26:28 +02:00
}
}