diff --git a/MLEM.Ui/Elements/AutoScaledText.cs b/MLEM.Ui/Elements/AutoScaledText.cs new file mode 100644 index 0000000..c46c97c --- /dev/null +++ b/MLEM.Ui/Elements/AutoScaledText.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Extensions; +using MLEM.Font; + +namespace MLEM.Ui.Elements { + public class AutoScaledText : Element { + + private readonly IGenericFont font; + private readonly bool centered; + private float scale; + private string text; + + public IGenericFont Font => this.font ?? this.System.DefaultFont; + public string Text { + get => this.text; + set { + this.text = value; + this.SetDirty(); + } + } + public Color Color; + + public AutoScaledText(Anchor anchor, Vector2 size, string text, bool centered = false, Color? color = null, IGenericFont font = null) : base(anchor, size) { + this.Text = text; + this.centered = centered; + this.font = font; + this.Color = color ?? Color.White; + } + + public override void ForceUpdateArea() { + base.ForceUpdateArea(); + + this.scale = 0; + Vector2 measure; + do { + this.scale += 0.1F; + measure = this.Font.MeasureString(this.Text) * this.scale; + } while (measure.X <= this.DisplayArea.Size.X && measure.Y <= this.DisplayArea.Size.Y); + } + + public override void Draw(GameTime time, SpriteBatch batch, Color color) { + var pos = this.DisplayArea.Location.ToVector2() + this.DisplayArea.Size.ToVector2() / 2; + this.Font.DrawCenteredString(batch, this.Text, pos, this.scale, this.Color.CopyAlpha(color), true, true); + base.Draw(time, batch, color); + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/Elements/Button.cs b/MLEM.Ui/Elements/Button.cs new file mode 100644 index 0000000..64ce765 --- /dev/null +++ b/MLEM.Ui/Elements/Button.cs @@ -0,0 +1,39 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MLEM.Extensions; +using MLEM.Textures; + +namespace MLEM.Ui.Elements { + public class Button : Element { + + public NinePatch Texture; + public NinePatch HoveredTexture; + public Color HoveredColor; + public AutoScaledText Text; + + public Button(Anchor anchor, Vector2 size, NinePatch texture, string text = null, NinePatch hoveredTexture = null, Color? hoveredColor = null) : base(anchor, size) { + this.Texture = texture; + this.HoveredTexture = hoveredTexture; + this.HoveredColor = hoveredColor ?? Color.White; + + if (text != null) { + this.Text = new AutoScaledText(Anchor.Center, Vector2.One, text, true) { + IgnoresMouse = true + }; + this.AddChild(this.Text); + } + } + + public override void Draw(GameTime time, SpriteBatch batch, Color color) { + var tex = this.Texture; + if (this.IsMouseOver) { + if (this.HoveredTexture != null) + tex = this.HoveredTexture; + color = this.HoveredColor.CopyAlpha(color); + } + batch.Draw(tex, this.DisplayArea, color); + base.Draw(time, batch, color); + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index eb89f39..426c0cf 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; +using MLEM.Input; namespace MLEM.Ui.Elements { public abstract class Element { + private readonly List children = new List(); private Anchor anchor; private Vector2 size; private Point offset; @@ -48,10 +50,16 @@ namespace MLEM.Ui.Elements { } } + public MouseClickCallback OnClicked; + public MouseClickCallback OnMouseDown; + public MouseCallback OnMouseEnter; + public MouseCallback OnMouseExit; + public UiSystem System { get; private set; } public Element Parent { get; private set; } + public bool IsMouseOver { get; private set; } public bool IsHidden; - private readonly List children = new List(); + public bool IgnoresMouse; private Rectangle area; public Rectangle Area { @@ -72,10 +80,13 @@ namespace MLEM.Ui.Elements { } private bool areaDirty; - public Element(Anchor anchor, Vector2 size, Point positionOffset) { + public Element(Anchor anchor, Vector2 size) { this.anchor = anchor; this.size = size; - this.offset = positionOffset; + + this.OnMouseEnter += (element, mousePos) => this.IsMouseOver = true; + this.OnMouseExit += (element, mousePos) => this.IsMouseOver = false; + this.SetDirty(); } @@ -238,8 +249,25 @@ namespace MLEM.Ui.Elements { public void SetUiSystem(UiSystem system) { this.System = system; foreach (var child in this.children) - child.System = system; + child.SetUiSystem(system); } + public Element GetMousedElement(Vector2 mousePos) { + if (this.IsHidden || this.IgnoresMouse) + return null; + if (!this.Area.Contains(mousePos)) + return null; + foreach (var child in this.children) { + var element = child.GetMousedElement(mousePos); + if (element != null) + return element; + } + return this; + } + + public delegate void MouseClickCallback(Element element, Vector2 mousePos, MouseButton button); + + public delegate void MouseCallback(Element element, Vector2 mousePos); + } } \ No newline at end of file diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index 2e6b84f..47677dd 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -7,8 +7,9 @@ namespace MLEM.Ui.Elements { private readonly NinePatch texture; - public Panel(Anchor anchor, Vector2 size, Point positionOffset, NinePatch texture) : base(anchor, size, positionOffset) { + public Panel(Anchor anchor, Vector2 size, Point positionOffset, NinePatch texture) : base(anchor, size) { this.texture = texture; + this.PositionOffset = positionOffset; this.ChildPadding = new Point(5); } diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index 03a6014..b6af4fa 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -26,7 +26,7 @@ namespace MLEM.Ui.Elements { } public IGenericFont Font => this.font ?? this.System.DefaultFont; - public Paragraph(Anchor anchor, float width, Point positionOffset, string text, float textScale = 1, bool centerText = false, IGenericFont font = null) : base(anchor, new Vector2(width, 0), positionOffset) { + public Paragraph(Anchor anchor, float width, string text, float textScale = 1, bool centerText = false, IGenericFont font = null) : base(anchor, new Vector2(width, 0)) { this.text = text; this.font = font; this.TextScale = textScale; diff --git a/MLEM.Ui/Elements/VerticalSpace.cs b/MLEM.Ui/Elements/VerticalSpace.cs new file mode 100644 index 0000000..13cb800 --- /dev/null +++ b/MLEM.Ui/Elements/VerticalSpace.cs @@ -0,0 +1,10 @@ +using Microsoft.Xna.Framework; + +namespace MLEM.Ui.Elements { + public class VerticalSpace : Element { + + public VerticalSpace(int height) : base(Anchor.AutoCenter, new Vector2(1, height)) { + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs index d2104fa..1b201b8 100644 --- a/MLEM.Ui/UiSystem.cs +++ b/MLEM.Ui/UiSystem.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Font; +using MLEM.Input; using MLEM.Ui.Elements; namespace MLEM.Ui { @@ -11,6 +12,8 @@ namespace MLEM.Ui { private readonly GraphicsDevice graphicsDevice; private readonly List rootElements = new List(); + private readonly InputHandler inputHandler; + private readonly bool isInputOurs; public readonly float GlobalScale; public readonly IGenericFont DefaultFont; @@ -20,11 +23,15 @@ namespace MLEM.Ui { return new Rectangle(bounds.X, bounds.Y, (bounds.Width / this.GlobalScale).Floor(), (bounds.Height / this.GlobalScale).Floor()); } } + public Vector2 MousePos => this.inputHandler.MousePosition.ToVector2() / this.GlobalScale; + public Element MousedElement { get; private set; } - public UiSystem(GameWindow window, GraphicsDevice device, float scale, IGenericFont defaultFont) { + public UiSystem(GameWindow window, GraphicsDevice device, float scale, IGenericFont defaultFont, InputHandler inputHandler = null) { this.graphicsDevice = device; this.GlobalScale = scale; this.DefaultFont = defaultFont; + this.inputHandler = inputHandler ?? new InputHandler(); + this.isInputOurs = inputHandler == null; window.ClientSizeChanged += (sender, args) => { foreach (var root in this.rootElements) @@ -33,6 +40,27 @@ namespace MLEM.Ui { } public void Update(GameTime time) { + if (this.isInputOurs) + this.inputHandler.Update(); + + var mousedNow = this.GetMousedElement(); + if (mousedNow != this.MousedElement) { + if (this.MousedElement != null) + this.MousedElement.OnMouseExit?.Invoke(this.MousedElement, this.MousePos); + if (mousedNow != null) + mousedNow.OnMouseEnter?.Invoke(mousedNow, this.MousePos); + this.MousedElement = mousedNow; + } + + if (mousedNow != null && (mousedNow.OnMouseDown != null || mousedNow.OnClicked != null)) { + foreach (var button in InputHandler.MouseButtons) { + if (mousedNow.OnMouseDown != null && this.inputHandler.IsMouseButtonDown(button)) + mousedNow.OnMouseDown(mousedNow, this.MousePos, button); + if (mousedNow.OnClicked != null && this.inputHandler.IsMouseButtonPressed(button)) + mousedNow.OnClicked(mousedNow, this.MousePos, button); + } + } + foreach (var root in this.rootElements) root.Element.Update(time); } @@ -77,6 +105,15 @@ namespace MLEM.Ui { return this.rootElements.FindIndex(element => element.Name == name); } + private Element GetMousedElement() { + foreach (var root in this.rootElements) { + var moused = root.Element.GetMousedElement(this.MousePos); + if (moused != null) + return moused; + } + return null; + } + } public struct RootElement { diff --git a/MLEM/Extensions/ColorExtensions.cs b/MLEM/Extensions/ColorExtensions.cs index 2f85987..a41553e 100644 --- a/MLEM/Extensions/ColorExtensions.cs +++ b/MLEM/Extensions/ColorExtensions.cs @@ -12,5 +12,9 @@ namespace MLEM.Extensions { return new Color((int) (value >> 16 & 0xFF), (int) (value >> 8 & 0xFF), (int) (value >> 0 & 0xFF), (int) (value >> 24 & 0xFF)); } + public static Color CopyAlpha(this Color color, Color other) { + return color * (other.A / 255F); + } + } } \ No newline at end of file diff --git a/MLEM/Input/InputHandler.cs b/MLEM/Input/InputHandler.cs index a996fae..359a95d 100644 --- a/MLEM/Input/InputHandler.cs +++ b/MLEM/Input/InputHandler.cs @@ -1,10 +1,15 @@ using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; +using MLEM.Misc; namespace MLEM.Input { public class InputHandler { + public static MouseButton[] MouseButtons = EnumHelper.GetValues().ToArray(); + public KeyboardState LastKeyboardState { get; private set; } public KeyboardState KeyboardState { get; private set; } private readonly bool handleKeyboard; diff --git a/MLEM/Misc/EnumHelper.cs b/MLEM/Misc/EnumHelper.cs new file mode 100644 index 0000000..6d7905b --- /dev/null +++ b/MLEM/Misc/EnumHelper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MLEM.Misc { + public static class EnumHelper { + + public static IEnumerable GetValues() { + return Enum.GetValues(typeof(T)).Cast(); + } + + } +} \ No newline at end of file diff --git a/Tests/Content/Textures/Test.png b/Tests/Content/Textures/Test.png index 742c6da..cec603e 100644 Binary files a/Tests/Content/Textures/Test.png and b/Tests/Content/Textures/Test.png differ diff --git a/Tests/GameImpl.cs b/Tests/GameImpl.cs index 7429029..767463a 100644 --- a/Tests/GameImpl.cs +++ b/Tests/GameImpl.cs @@ -18,16 +18,26 @@ namespace Tests { private NinePatch testPatch; private UiSystem uiSystem; + public GameImpl() { + this.IsMouseVisible = true; + } + protected override void LoadContent() { base.LoadContent(); this.testTexture = LoadContent("Textures/Test"); this.testPatch = new NinePatch(new TextureRegion(this.testTexture, 0, 8, 24, 24), 8); // Ui system tests - this.uiSystem = new UiSystem(this.Window, this.GraphicsDevice, 5, new GenericSpriteFont(LoadContent("Fonts/TestFont"))); + this.uiSystem = new UiSystem(this.Window, this.GraphicsDevice, 5, new GenericSpriteFont(LoadContent("Fonts/TestFont")), this.InputHandler); var root = new Panel(Anchor.BottomLeft, new Vector2(100, 100), new Point(5, 5), this.testPatch); - root.AddChild(new Paragraph(Anchor.AutoLeft, 1, Point.Zero, "This is a test text that is hopefully long enough to cover at least a few lines, otherwise it would be very sad.", 0.2F)); + root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a test text that is hopefully long enough to cover at least a few lines, otherwise it would be very sad.", 0.2F)); + root.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 15), new NinePatch(new TextureRegion(this.testTexture, 24, 8, 16, 16), 4), "Test Button") { + OnClicked = (element, pos, button) => { + if (button == MouseButton.Left) + Console.WriteLine("Clicked"); + } + }); this.uiSystem.Add("Test", root); }