diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs new file mode 100644 index 0000000..376ddaf --- /dev/null +++ b/MLEM.Ui/Elements/Element.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Extensions; + +namespace MLEM.Ui.Elements { + public class Element { + + private Anchor anchor; + private Point size; + private Point offset; + private Point padding; + public Anchor Anchor { + get => this.anchor; + set { + this.anchor = value; + this.SetDirty(); + } + } + public Point Size { + get => this.size; + set { + this.size = value; + this.SetDirty(); + } + } + public Point PositionOffset { + get => this.offset; + set { + this.offset = value; + this.SetDirty(); + } + } + public Point Padding { + get => this.padding; + set { + this.padding = value; + this.SetDirty(); + } + } + + public UiSystem System; + public Element Parent { get; private set; } + private readonly List children = new List(); + + 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; + } + + } + protected bool AreaDirty; + + public Element(Anchor anchor, Point size, Point positionOffset) { + this.anchor = anchor; + this.size = size; + this.offset = positionOffset; + } + + public void AddChild(Element element) { + this.children.Add(element); + element.Parent = this; + this.SetDirty(); + } + + public void RemoveChild(Element element) { + this.children.Remove(element); + element.Parent = null; + this.SetDirty(); + } + + public void SetDirty() { + this.AreaDirty = true; + } + + public void UpdateAreaIfDirty() { + if (this.AreaDirty) + this.ForceUpdateArea(); + } + + public void ForceUpdateArea() { + this.AreaDirty = false; + + var parentArea = this.Parent != null ? this.Parent.area : this.System.ScaledViewport; + var parentCenterX = parentArea.X + parentArea.Width / 2; + var parentCenterY = parentArea.Y + parentArea.Height / 2; + + var actualSize = this.CalcActualSize(); + 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) { + var previousChild = this.GetPreviousChild(); + 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(); + } + + private Point CalcActualSize() { + return this.size; + } + + public Element GetPreviousChild() { + if (this.Parent == null) + return null; + + Element lastChild = null; + foreach (var child in this.Parent.children) { + if (child == this) + break; + lastChild = child; + } + return lastChild; + } + + public void Update(GameTime time) { + foreach (var child in this.children) + child.Update(time); + } + + public void Draw(GameTime time, SpriteBatch batch) { + batch.Draw(batch.GetBlankTexture(), this.DisplayArea, this.Parent == null ? Color.Blue : Color.Red); + + foreach (var child in this.children) + child.Draw(time, batch); + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs new file mode 100644 index 0000000..17eefcc --- /dev/null +++ b/MLEM.Ui/UiSystem.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Extensions; +using MLEM.Ui.Elements; + +namespace MLEM.Ui { + public class UiSystem { + + private readonly GraphicsDevice graphicsDevice; + private readonly List rootElements = new List(); + + public float GlobalScale; + public Rectangle ScaledViewport { + get { + var bounds = this.graphicsDevice.Viewport.Bounds; + return new Rectangle(bounds.X, bounds.Y, (bounds.Width / this.GlobalScale).Floor(), (bounds.Height / this.GlobalScale).Floor()); + } + } + + public UiSystem(GameWindow window, GraphicsDevice device, float scale) { + this.graphicsDevice = device; + this.GlobalScale = scale; + window.ClientSizeChanged += (sender, args) => { + foreach (var root in this.rootElements) + root.Element.ForceUpdateArea(); + }; + } + + public void Update(GameTime time) { + foreach (var root in this.rootElements) + root.Element.Update(time); + } + + public void Draw(GameTime time, SpriteBatch batch, BlendState blendState = null, SamplerState samplerState = null) { + batch.Begin(SpriteSortMode.Deferred, blendState, samplerState, transformMatrix: Matrix.CreateScale(this.GlobalScale)); + foreach (var root in this.rootElements) + root.Element.Draw(time, batch); + batch.End(); + } + + public void Add(string name, Element root) { + if (this.IndexOf(name) >= 0) + throw new ArgumentException($"There is already a root element with name {name}"); + + this.rootElements.Add(new RootElement(name, root)); + root.System = this; + } + + public void Remove(string name) { + var index = this.IndexOf(name); + if (index < 0) + return; + this.rootElements.RemoveAt(index); + } + + public Element Get(string name) { + var index = this.IndexOf(name); + return index < 0 ? null : this.rootElements[index].Element; + } + + private int IndexOf(string name) { + return this.rootElements.FindIndex(element => element.Name == name); + } + + } + + public struct RootElement { + + public readonly string Name; + public readonly Element Element; + + public RootElement(string name, Element element) { + this.Name = name; + this.Element = element; + } + + } +} \ No newline at end of file diff --git a/Tests/GameImpl.cs b/Tests/GameImpl.cs index 57bcceb..7bcc62b 100644 --- a/Tests/GameImpl.cs +++ b/Tests/GameImpl.cs @@ -6,6 +6,8 @@ using MLEM.Extended.Extensions; using MLEM.Input; using MLEM.Startup; using MLEM.Textures; +using MLEM.Ui; +using MLEM.Ui.Elements; using MonoGame.Extended.TextureAtlases; namespace Tests { @@ -16,12 +18,25 @@ namespace Tests { private NinePatch testPatch; private NinePatchRegion2D extendedPatch; + private UiSystem uiSystem; + private Element testChild; + protected override void LoadContent() { base.LoadContent(); this.testTexture = LoadContent("Textures/Test"); this.testRegion = new TextureRegion(this.testTexture, 32, 0, 8, 8); this.testPatch = new NinePatch(new TextureRegion(this.testTexture, 0, 8, 24, 24), 8); this.extendedPatch = this.testPatch.ToExtended(); + + // Ui system tests + this.uiSystem = new UiSystem(this.Window, this.GraphicsDevice, 5); + + var root = new Element(Anchor.BottomLeft, new Point(100, 100), new Point(5, 5)); + for (var i = 0; i < 3; i++) + root.AddChild(new Element(Anchor.AutoInline, new Point(16, 16), Point.Zero) { + Padding = new Point(1, 1) + }); + this.uiSystem.Add("Test", root); } protected override void Update(GameTime gameTime) { @@ -34,6 +49,8 @@ namespace Tests { Console.WriteLine("Left was pressed"); if (Input.IsGamepadButtonPressed(0, Buttons.A)) Console.WriteLine("Gamepad A was pressed"); + + this.uiSystem.Update(gameTime); } protected override void Draw(GameTime gameTime) { @@ -49,6 +66,8 @@ namespace Tests { this.SpriteBatch.Draw(this.testPatch, new Rectangle(20, 20, 40, 20), Color.White); this.SpriteBatch.Draw(this.extendedPatch, new Rectangle(80, 20, 40, 20), Color.White); this.SpriteBatch.End(); + + this.uiSystem.Draw(gameTime, this.SpriteBatch); } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 82aed58..61edf6a 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -6,19 +6,20 @@ - - - + + + + - - + + - - + +