diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index a99c2ad..322a608 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -86,7 +86,8 @@ namespace Demos { // (like above), these custom values don't get undone HasCustomStyle = true, Texture = this.testPatch, - HoveredColor = Color.LightGray + HoveredColor = Color.LightGray, + SelectionIndicator = style.SelectionIndicator }); root.AddChild(new VerticalSpace(3)); @@ -129,7 +130,7 @@ namespace Demos { root.AddChild(new RadioButton(Anchor.AutoLeft, new Vector2(1, 10), "Radio button 2!") {PositionOffset = new Vector2(0, 1)}); var tooltip = new Tooltip(50, "This is a test tooltip to see the window bounding") {IsHidden = true}; - this.UiSystem.Add("TestTooltip", tooltip); + this.UiSystem.Add("TestTooltip", tooltip).CanSelectContent = false; root.AddChild(new VerticalSpace(3)); root.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), "Toggle Test Tooltip") { OnPressed = element => tooltip.IsHidden = !tooltip.IsHidden diff --git a/MLEM.Ui/Elements/Element.cs b/MLEM.Ui/Elements/Element.cs index 1130bc5..fed4503 100644 --- a/MLEM.Ui/Elements/Element.cs +++ b/MLEM.Ui/Elements/Element.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MLEM.Extensions; using MLEM.Input; +using MLEM.Textures; using MLEM.Ui.Style; namespace MLEM.Ui.Elements { @@ -98,11 +99,11 @@ namespace MLEM.Ui.Elements { this.InitStyle(this.system.Style); } } - protected InputHandler Input => this.System.InputHandler; protected UiControls Controls => this.System.Controls; + protected InputHandler Input => this.Controls.Input; + public Point MousePos => this.Input.MousePosition; public RootElement Root { get; private set; } public float Scale => this.Root.ActualScale; - public Point MousePos => this.Input.MousePosition; public Element Parent { get; private set; } public bool IsMouseOver { get; private set; } public bool IsSelected { get; private set; } @@ -116,7 +117,8 @@ namespace MLEM.Ui.Elements { this.SetAreaDirty(); } } - public bool IgnoresMouse; + public bool CanBeSelected = true; + public bool CanBeMoused = true; public float DrawAlpha = 1; public bool HasCustomStyle; public bool SetHeightBasedOnChildren; @@ -153,6 +155,7 @@ namespace MLEM.Ui.Elements { } private bool areaDirty; private bool sortedChildrenDirty; + public NinePatch SelectionIndicator; public Element(Anchor anchor, Vector2 size) { this.anchor = anchor; @@ -422,6 +425,10 @@ namespace MLEM.Ui.Elements { if (!child.IsHidden) child.Draw(time, batch, alpha * child.DrawAlpha, offset); } + + if (this.IsSelected && this.Controls.ShowSelectionIndicator && this.SelectionIndicator != null) { + batch.Draw(this.SelectionIndicator, this.DisplayArea.OffsetCopy(offset), Color.White * alpha); + } } public virtual void DrawEarly(GameTime time, SpriteBatch batch, float alpha, BlendState blendState = null, SamplerState samplerState = null) { @@ -432,17 +439,18 @@ namespace MLEM.Ui.Elements { } public virtual Element GetMousedElement() { - if (this.IsHidden || this.IgnoresMouse) + if (this.IsHidden) return null; for (var i = this.SortedChildren.Count - 1; i >= 0; i--) { var element = this.SortedChildren[i].GetMousedElement(); if (element != null) return element; } - return this.Area.Contains(this.MousePos) ? this : null; + return this.CanBeMoused && this.Area.Contains(this.MousePos) ? this : null; } protected virtual void InitStyle(UiStyle style) { + this.SelectionIndicator = style.SelectionIndicator; } public delegate void TextInputCallback(Element element, Keys key, char character); diff --git a/MLEM.Ui/Elements/ElementHelper.cs b/MLEM.Ui/Elements/ElementHelper.cs index 294241f..54bf81a 100644 --- a/MLEM.Ui/Elements/ElementHelper.cs +++ b/MLEM.Ui/Elements/ElementHelper.cs @@ -7,10 +7,7 @@ namespace MLEM.Ui.Elements { public static Button ImageButton(Anchor anchor, Vector2 size, TextureRegion texture, string text = null, string tooltipText = null, float tooltipWidth = 50, int imagePadding = 2) { var button = new Button(anchor, size, text, tooltipText, tooltipWidth); - var image = new Image(Anchor.CenterLeft, Vector2.One, texture) { - Padding = new Point(imagePadding), - IgnoresMouse = true - }; + var image = new Image(Anchor.CenterLeft, Vector2.One, texture) {Padding = new Point(imagePadding)}; button.OnAreaUpdated += e => image.Size = new Vector2(e.Area.Height, e.Area.Height) / e.Scale; button.AddChild(image, 0); return button; diff --git a/MLEM.Ui/Elements/Group.cs b/MLEM.Ui/Elements/Group.cs index a852408..fc5d378 100644 --- a/MLEM.Ui/Elements/Group.cs +++ b/MLEM.Ui/Elements/Group.cs @@ -5,6 +5,7 @@ namespace MLEM.Ui.Elements { public Group(Anchor anchor, Vector2 size, bool setHeightBasedOnChildren = true) : base(anchor, size) { this.SetHeightBasedOnChildren = setHeightBasedOnChildren; + this.CanBeSelected = false; } } diff --git a/MLEM.Ui/Elements/Image.cs b/MLEM.Ui/Elements/Image.cs index 3bf5f78..243336c 100644 --- a/MLEM.Ui/Elements/Image.cs +++ b/MLEM.Ui/Elements/Image.cs @@ -15,6 +15,7 @@ namespace MLEM.Ui.Elements { public Image(Anchor anchor, Vector2 size, TextureRegion texture, bool scaleToImage = false) : base(anchor, size) { this.Texture = texture; this.ScaleToImage = scaleToImage; + this.CanBeSelected = false; } protected override Point CalcActualSize(Rectangle parentArea) { diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index 54327e3..0eef8ba 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -19,6 +19,7 @@ namespace MLEM.Ui.Elements { this.SetHeightBasedOnChildren = setHeightBasedOnChildren; this.scrollOverflow = scrollOverflow; this.ChildPadding = new Point(5); + this.CanBeSelected = false; if (scrollOverflow) { var scrollSize = scrollerSize ?? Point.Zero; diff --git a/MLEM.Ui/Elements/Paragraph.cs b/MLEM.Ui/Elements/Paragraph.cs index 0573db6..81d70f4 100644 --- a/MLEM.Ui/Elements/Paragraph.cs +++ b/MLEM.Ui/Elements/Paragraph.cs @@ -45,7 +45,8 @@ namespace MLEM.Ui.Elements { public Paragraph(Anchor anchor, float width, string text, bool centerText = false) : base(anchor, new Vector2(width, 0)) { this.text = text; this.AutoAdjustWidth = centerText; - this.IgnoresMouse = true; + this.CanBeSelected = false; + this.CanBeMoused = false; } protected override Point CalcActualSize(Rectangle parentArea) { diff --git a/MLEM.Ui/Elements/ScrollBar.cs b/MLEM.Ui/Elements/ScrollBar.cs index 924a6b2..6569a61 100644 --- a/MLEM.Ui/Elements/ScrollBar.cs +++ b/MLEM.Ui/Elements/ScrollBar.cs @@ -43,14 +43,15 @@ namespace MLEM.Ui.Elements { this.maxValue = maxValue; this.Horizontal = horizontal; this.ScrollerSize = new Point(horizontal ? scrollerSize : size.X.Floor(), !horizontal ? scrollerSize : size.Y.Floor()); + this.CanBeSelected = false; } public override void Update(GameTime time) { base.Update(time); - var moused = this.System.MousedElement; - if (moused == this && this.Controls.MainButton(this.Input)) { + var moused = this.Controls.MousedElement; + if (moused == this && this.Controls.Input.IsMouseButtonDown(MouseButton.Left)) { this.isMouseHeld = true; - } else if (this.isMouseHeld && this.Controls.MainButton(this.Input)) { + } else if (this.isMouseHeld && !this.Controls.Input.IsMouseButtonDown(MouseButton.Left)) { this.isMouseHeld = false; } @@ -65,7 +66,7 @@ namespace MLEM.Ui.Elements { } if (!this.Horizontal && moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { - var scroll = this.Controls.Scroll(this.Input); + var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; if (scroll != 0) this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); } diff --git a/MLEM.Ui/Elements/Tooltip.cs b/MLEM.Ui/Elements/Tooltip.cs index d69a8f2..d6fd273 100644 --- a/MLEM.Ui/Elements/Tooltip.cs +++ b/MLEM.Ui/Elements/Tooltip.cs @@ -13,9 +13,10 @@ namespace MLEM.Ui.Elements { base(Anchor.TopLeft, width, text) { this.AutoAdjustWidth = true; this.Padding = new Point(2); + this.CanBeSelected = false; if (elementToHover != null) { - elementToHover.OnMouseEnter += element => element.System.Add(element.GetType().Name + "Tooltip", this); + elementToHover.OnMouseEnter += element => element.System.Add(element.GetType().Name + "Tooltip", this).CanSelectContent = false; elementToHover.OnMouseExit += element => element.System.Remove(element.GetType().Name + "Tooltip"); } } diff --git a/MLEM.Ui/Elements/VerticalSpace.cs b/MLEM.Ui/Elements/VerticalSpace.cs index 13cb800..dcf004d 100644 --- a/MLEM.Ui/Elements/VerticalSpace.cs +++ b/MLEM.Ui/Elements/VerticalSpace.cs @@ -4,6 +4,7 @@ namespace MLEM.Ui.Elements { public class VerticalSpace : Element { public VerticalSpace(int height) : base(Anchor.AutoCenter, new Vector2(1, height)) { + this.CanBeSelected = false; } } diff --git a/MLEM.Ui/Style/UiStyle.cs b/MLEM.Ui/Style/UiStyle.cs index 1ea536c..c4358bc 100644 --- a/MLEM.Ui/Style/UiStyle.cs +++ b/MLEM.Ui/Style/UiStyle.cs @@ -5,6 +5,7 @@ using MLEM.Textures; namespace MLEM.Ui.Style { public class UiStyle { + public NinePatch SelectionIndicator; public NinePatch ButtonTexture; public NinePatch ButtonHoveredTexture; public Color ButtonHoveredColor; @@ -28,5 +29,6 @@ namespace MLEM.Ui.Style { public IGenericFont BoldFont; public IGenericFont ItalicFont; public float TextScale = 1; + } } \ No newline at end of file diff --git a/MLEM.Ui/Style/UntexturedStyle.cs b/MLEM.Ui/Style/UntexturedStyle.cs index 63e9e38..59c2159 100644 --- a/MLEM.Ui/Style/UntexturedStyle.cs +++ b/MLEM.Ui/Style/UntexturedStyle.cs @@ -9,6 +9,7 @@ namespace MLEM.Ui.Style { public class UntexturedStyle : UiStyle { public UntexturedStyle(SpriteBatch batch) { + this.SelectionIndicator = GenerateTexture(batch, Color.Transparent, Color.Red); this.ButtonTexture = GenerateTexture(batch, Color.CadetBlue); this.ButtonHoveredColor = Color.LightGray; this.PanelTexture = GenerateTexture(batch, Color.Gray); @@ -27,12 +28,13 @@ namespace MLEM.Ui.Style { this.Font = new EmptyFont(); } - private static NinePatch GenerateTexture(SpriteBatch batch, Color color) { + private static NinePatch GenerateTexture(SpriteBatch batch, Color color, Color? outlineColor = null) { + var outli = outlineColor ?? Color.Black; var tex = new Texture2D(batch.GraphicsDevice, 3, 3); tex.SetData(new[] { - Color.Black, Color.Black, Color.Black, - Color.Black, color, Color.Black, - Color.Black, Color.Black, Color.Black + outli, outli, outli, + outli, color, outli, + outli, outli, outli }); batch.Disposing += (sender, args) => { if (tex != null) { diff --git a/MLEM.Ui/UiControls.cs b/MLEM.Ui/UiControls.cs index 0f665fa..62ed326 100644 --- a/MLEM.Ui/UiControls.cs +++ b/MLEM.Ui/UiControls.cs @@ -1,15 +1,116 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; using MLEM.Input; +using MLEM.Ui.Elements; namespace MLEM.Ui { public class UiControls { - public BoolQuery MainButton = h => h.IsMouseButtonPressed(MouseButton.Left); - public BoolQuery SecondaryButton = h => h.IsMouseButtonPressed(MouseButton.Right); - public FloatQuery Scroll = h => h.LastScrollWheel - h.ScrollWheel; + public readonly InputHandler Input; + private readonly bool isInputOurs; + private readonly UiSystem system; - public delegate bool BoolQuery(InputHandler handler); + public Element MousedElement { get; private set; } + public Element SelectedElement { get; private set; } + public bool ShowSelectionIndicator; - public delegate float FloatQuery(InputHandler handler); + public UiControls(UiSystem system, InputHandler inputHandler = null) { + this.system = system; + this.Input = inputHandler ?? new InputHandler(); + this.isInputOurs = inputHandler == null; + } + + public void Update() { + if (this.isInputOurs) + this.Input.Update(); + + var mousedNow = this.GetMousedElement(); + // mouse new element + if (mousedNow != this.MousedElement) { + if (this.MousedElement != null) + this.MousedElement.OnMouseExit?.Invoke(this.MousedElement); + if (mousedNow != null) + mousedNow.OnMouseEnter?.Invoke(mousedNow); + this.MousedElement = mousedNow; + } + + if (this.Input.IsMouseButtonPressed(MouseButton.Left)) { + // select element + var selectedNow = mousedNow != null && mousedNow.CanBeSelected ? mousedNow : null; + if (this.SelectedElement != selectedNow) + this.SelectElement(selectedNow, false); + + // first action on element + if (mousedNow != null) + mousedNow.OnPressed?.Invoke(mousedNow); + } else if (this.Input.IsMouseButtonPressed(MouseButton.Right)) { + // secondary action on element + if (mousedNow != null) + mousedNow.OnSecondaryPressed?.Invoke(mousedNow); + } else if (this.Input.IsKeyPressed(Keys.Enter) || this.Input.IsKeyPressed(Keys.Space)) { + if (this.SelectedElement != null) { + if (this.Input.IsModifierKeyDown(ModifierKey.Shift)) { + // secondary action on element using space or enter + this.SelectedElement.OnSecondaryPressed?.Invoke(this.SelectedElement); + } else { + // first action on element using space or enter + this.SelectedElement.OnPressed?.Invoke(this.SelectedElement); + } + } + } else if (this.Input.IsKeyPressed(Keys.Tab)) { + // tab or shift-tab to next or previous element + this.SelectElement(this.GetNextElement(this.Input.IsModifierKeyDown(ModifierKey.Shift)), true); + } + } + + public void SelectElement(Element element, bool show) { + if (this.SelectedElement != null) + this.SelectedElement.OnDeselected?.Invoke(this.SelectedElement); + if (element != null) + element.OnSelected?.Invoke(element); + this.SelectedElement = element; + this.ShowSelectionIndicator = show; + } + + public Element GetMousedElement() { + foreach (var root in this.system.GetRootElements()) { + var moused = root.Element.GetMousedElement(); + if (moused != null) + return moused; + } + return null; + } + + private Element GetNextElement(bool backward) { + var currRoot = this.system.GetRootElements().FirstOrDefault(root => root.CanSelectContent); + if (currRoot == null) + return null; + var children = currRoot.Element.GetChildren(regardChildrensChildren: true); + if (this.SelectedElement == null || this.SelectedElement.Root != currRoot) { + return backward ? children.LastOrDefault(c => c.CanBeSelected) : children.FirstOrDefault(c => c.CanBeSelected); + } else { + var foundCurr = false; + Element lastFound = null; + foreach (var child in children) { + if (!child.CanBeSelected) + continue; + if (child == this.SelectedElement) { + // when going backwards, return the last element found before the current one + if (backward) + return lastFound; + foundCurr = true; + } else { + // when going forwards, return the element after the current one + if (!backward && foundCurr) + return child; + } + lastFound = child; + } + return null; + } + } } } \ No newline at end of file diff --git a/MLEM.Ui/UiSystem.cs b/MLEM.Ui/UiSystem.cs index 3a2a4ae..e2d1293 100644 --- a/MLEM.Ui/UiSystem.cs +++ b/MLEM.Ui/UiSystem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; @@ -14,8 +15,6 @@ namespace MLEM.Ui { public readonly GraphicsDevice GraphicsDevice; public Rectangle Viewport { get; private set; } private readonly List rootElements = new List(); - public readonly InputHandler InputHandler; - private readonly bool isInputOurs; public bool AutoScaleWithScreen; public Point AutoScaleReferenceSize; @@ -33,8 +32,6 @@ namespace MLEM.Ui { root.Element.ForceUpdateArea(); } } - public Element MousedElement { get; private set; } - public Element SelectedElement { get; private set; } private UiStyle style; public UiStyle Style { get => this.style; @@ -49,12 +46,11 @@ namespace MLEM.Ui { public float DrawAlpha = 1; public BlendState BlendState; public SamplerState SamplerState = SamplerState.PointClamp; - public UiControls Controls = new UiControls(); + public UiControls Controls; public UiSystem(GameWindow window, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) { + this.Controls = new UiControls(this, inputHandler); this.GraphicsDevice = device; - this.InputHandler = inputHandler ?? new InputHandler(); - this.isInputOurs = inputHandler == null; this.style = style; this.Viewport = device.Viewport.Bounds; this.AutoScaleReferenceSize = this.Viewport.Size; @@ -71,37 +67,7 @@ namespace MLEM.Ui { } public void Update(GameTime time) { - if (this.isInputOurs) - this.InputHandler.Update(); - - var mousedNow = this.GetMousedElement(); - // mouse new element - if (mousedNow != this.MousedElement) { - if (this.MousedElement != null) - this.MousedElement.OnMouseExit?.Invoke(this.MousedElement); - if (mousedNow != null) - mousedNow.OnMouseEnter?.Invoke(mousedNow); - this.MousedElement = mousedNow; - } - - if (this.Controls.MainButton(this.InputHandler)) { - // select element - if (this.SelectedElement != mousedNow) { - if (this.SelectedElement != null) - this.SelectedElement.OnDeselected?.Invoke(this.SelectedElement); - if (mousedNow != null) - mousedNow.OnSelected?.Invoke(mousedNow); - this.SelectedElement = mousedNow; - } - - // first action on element - if (mousedNow != null) - mousedNow.OnPressed?.Invoke(mousedNow); - } else if (this.Controls.SecondaryButton(this.InputHandler)) { - // secondary action on element - if (mousedNow != null) - mousedNow.OnSecondaryPressed?.Invoke(mousedNow); - } + this.Controls.Update(); foreach (var root in this.rootElements) root.Element.Update(time); @@ -158,13 +124,9 @@ namespace MLEM.Ui { return this.rootElements.FindIndex(element => element.Name == name); } - private Element GetMousedElement() { - for (var i = this.rootElements.Count - 1; i >= 0; i--) { - var moused = this.rootElements[i].Element.GetMousedElement(); - if (moused != null) - return moused; - } - return null; + public IEnumerable GetRootElements() { + for (var i = this.rootElements.Count - 1; i >= 0; i--) + yield return this.rootElements[i]; } } @@ -185,6 +147,7 @@ namespace MLEM.Ui { } } public float ActualScale => this.System.GlobalScale * this.Scale; + public bool CanSelectContent = true; public RootElement(string name, Element element, UiSystem system) { this.Name = name; diff --git a/MLEM/Input/InputHandler.cs b/MLEM/Input/InputHandler.cs index 359a95d..e1639b7 100644 --- a/MLEM/Input/InputHandler.cs +++ b/MLEM/Input/InputHandler.cs @@ -77,6 +77,19 @@ namespace MLEM.Input { return this.WasKeyUp(key) && this.IsKeyDown(key); } + public bool IsModifierKeyDown(ModifierKey modifier) { + switch (modifier) { + case ModifierKey.Shift: + return this.IsKeyDown(Keys.LeftShift) || this.IsKeyDown(Keys.RightShift); + case ModifierKey.Control: + return this.IsKeyDown(Keys.LeftControl) || this.IsKeyDown(Keys.RightControl); + case ModifierKey.Alt: + return this.IsKeyDown(Keys.LeftAlt) || this.IsKeyDown(Keys.RightAlt); + default: + throw new ArgumentException(nameof(modifier)); + } + } + public bool IsMouseButtonDown(MouseButton button) { return GetState(this.MouseState, button) == ButtonState.Pressed; } @@ -145,4 +158,12 @@ namespace MLEM.Input { Extra2 } + + public enum ModifierKey { + + Shift, + Control, + Alt + + } } \ No newline at end of file