diff --git a/GeonBit.UI/GeonBit.UI.csproj b/GeonBit.UI/GeonBit.UI.csproj index 16ff9d8..6bcbaa1 100644 --- a/GeonBit.UI/GeonBit.UI.csproj +++ b/GeonBit.UI/GeonBit.UI.csproj @@ -86,12 +86,15 @@ + + + diff --git a/GeonBit.UI/Source/Systems/ISystem.cs b/GeonBit.UI/Source/Systems/ISystem.cs new file mode 100644 index 0000000..552321c --- /dev/null +++ b/GeonBit.UI/Source/Systems/ISystem.cs @@ -0,0 +1,15 @@ +namespace GeonBit.UI.Systems +{ + /// + /// System interface for performing some logic. + /// + public interface ISystem + { + + /// + /// Update logic for every frame. + /// + void Update(); + } + +} \ No newline at end of file diff --git a/GeonBit.UI/Source/Systems/TabList.cs b/GeonBit.UI/Source/Systems/TabList.cs new file mode 100644 index 0000000..7663210 --- /dev/null +++ b/GeonBit.UI/Source/Systems/TabList.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Linq; +using GeonBit.UI.Entities; +using GeonBit.UI.Validators; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace GeonBit.UI.Systems +{ + /// + /// Allows you to add an collection of entities to this list, and then have the ability to tab through them. + /// + public class TabList : ISystem + { + + /// + /// Wraps an entity for use with tab list, to keep track of an entities default properties. + /// + public class TabEntity + { + /// + /// The wrapped entity + /// + public Entity Entity { get; private set; } + /// + /// The entities default fill + /// + public Color Fill { get; private set; } + + /// + /// Wraps an entity with its default properties. + /// + /// The entity to wrap + public TabEntity(Entity entity) + { + Entity = entity; + Fill = entity.FillColor; + } + } + + private KeyboardState _lastKeyState; + private KeyboardState _keyState; + + private TabEntity[] _entities; + private readonly Keys _cycleKey; + private readonly Keys _selectKey; + private readonly bool _wraparound; + private readonly Color _cycleFill; + private int _currentSelection = -1; + + + /// + /// Creates a tab list for a given set of entities. + /// + /// The entities to store in the tab list + /// The fill color of the entity when cycled to + /// The key that must be pressed to focus next entity + /// The key that must be pressed to select entity + /// Whether or not tab will reset to zero at end of the list + public TabList(IEnumerable entities, Color cycleFill = default, Keys cycleKey = Keys.Tab, Keys selectKey = Keys.Enter, bool wraparound = true) + { + _cycleKey = cycleKey; + _selectKey = selectKey; + _wraparound = wraparound; + _cycleFill = cycleFill; + + SetupEntities(entities); + } + + /// + /// Performs cycle and select logic based on keyboard input. + /// + public void Update() + { + if (UserInterface.Active == null) return; + + _keyState = Keyboard.GetState(); + + if (KeyPressed(_cycleKey)) + { + CycleNext(); + } else if (KeyPressed(_selectKey)) + { + UseCurrent(); + } + + _lastKeyState = _keyState; + } + + /// + /// Creates a new list of entities and wraps them in an tab entity object. + /// + /// The entities to wrap. + private void SetupEntities(IEnumerable entities) + { + var enumerable = entities as Entity[] ?? entities.ToArray(); + AddIgnoreKeyValidator(enumerable); + _entities = enumerable.Select(entity => new TabEntity(entity)).ToArray(); + } + + /// + /// Add the ignore key validator to each applicable entity. + /// + /// + private void AddIgnoreKeyValidator(IEnumerable entities) + { + foreach (var tabEntity in entities) + { + if (tabEntity.GetType() != typeof(TextInput)) continue; + + var textInput = tabEntity as TextInput; + textInput?.Validators.Add(new IgnoreKeyValidator(_cycleKey)); + } + } + + /// + /// Resets the styling of a given entity to its original properties, and removes the focus. + /// + private void DeselectLastCycled() + { + if (!IsValidSelection()) return; + + _entities[_currentSelection].Entity.IsFocused = false; + _entities[_currentSelection].Entity.FillColor = _entities[_currentSelection].Fill; + } + + /// + /// Keeps the current selection in the bounds of the list, or wraps around to zero. + /// + private void ConstrainSelection() + { + if (_wraparound && _currentSelection >= _entities.Length) + _currentSelection = 0; + else + MathHelper.Clamp(_currentSelection, 0, _entities.Length); + } + + /// + /// Cycles to the next selection, removes focus from the old, and styles the new. + /// + private void CycleNext() + { + DeselectLastCycled(); + _currentSelection++; + ConstrainSelection(); + + _entities[_currentSelection].Entity.FillColor = _cycleFill; + _entities[_currentSelection].Entity.IsFocused = true; + } + + /// + /// Activates any of the actions of a given entity. + /// + private void UseCurrent() + { + if (!IsValidSelection()) return; + _entities[_currentSelection].Entity.OnClick?.Invoke(_entities[_currentSelection].Entity); + } + + /// + /// Checks if a key has been pressed this update. + /// + private bool KeyPressed(Keys key) => _keyState.IsKeyDown(key) && _lastKeyState.IsKeyUp(key); + + /// + /// Checks if the current selection is within the bounds of the list. + /// + private bool IsValidSelection() => _currentSelection >= 0 && _currentSelection < _entities.Length; + + } + +} \ No newline at end of file diff --git a/GeonBit.UI/Source/UserInterface.cs b/GeonBit.UI/Source/UserInterface.cs index c99ab81..deee7d8 100644 --- a/GeonBit.UI/Source/UserInterface.cs +++ b/GeonBit.UI/Source/UserInterface.cs @@ -8,12 +8,15 @@ // Since: 2016. //----------------------------------------------------------------------------- #endregion + +using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GeonBit.UI.Entities; using Microsoft.Xna.Framework.Content; using System.Runtime.Serialization; using System.Xml.Serialization; +using GeonBit.UI.Systems; namespace GeonBit.UI @@ -183,6 +186,11 @@ public RenderTarget2D RenderTarget /// public RootPanel Root { get; private set; } + /// + /// A list of systems attached to the user interface. + /// + public IList Systems { get; private set; } + /// /// Blend state to use when rendering UI. /// @@ -436,6 +444,8 @@ public UserInterface() // create the root panel Root = new RootPanel(); + + Systems = new List(); // set default cursor SetCursor(CursorType.Default); @@ -506,12 +516,31 @@ public void RemoveEntity(Entity entity) Root.RemoveChild(entity); } + /// + /// Adds a system to the user interface. + /// + /// The system to add. + public void AddSystem(ISystem system) + { + Systems.Add(system); + } + + /// + /// Removes a system from the user interface. + /// + /// The system to remove from the user interface. + public void RemoveSystem(ISystem system) + { + Systems.Remove(system); + } + /// /// Remove all entities from screen. /// public void Clear() { Root.ClearChildren(); + Systems.Clear(); } /// @@ -537,6 +566,8 @@ public void Update(GameTime gameTime) bool wasEventHandled = false; Root.Update(ref target, ref _dragTarget, ref wasEventHandled, Point.Zero); + UpdateSystems(); + // set active entity if (MouseInputProvider.MouseButtonDown(MouseButton.Left)) { @@ -553,6 +584,17 @@ public void Update(GameTime gameTime) TargetEntity = target; } + /// + /// Updates each system attached to the user interface. + /// + private void UpdateSystems() + { + foreach (var system in Systems) + { + system.Update(); + } + } + /// /// Update tooltip text related stuff. /// diff --git a/GeonBit.UI/Source/Validators/IgnoreKeyValidator.cs b/GeonBit.UI/Source/Validators/IgnoreKeyValidator.cs new file mode 100644 index 0000000..94e37ad --- /dev/null +++ b/GeonBit.UI/Source/Validators/IgnoreKeyValidator.cs @@ -0,0 +1,33 @@ +using GeonBit.UI.Entities.TextValidators; +using Microsoft.Xna.Framework.Input; + +namespace GeonBit.UI.Validators +{ + /// + /// Ignores a given key + /// + public class IgnoreKeyValidator : ITextValidator + { + private readonly Keys _key; + + /// + /// Ignores a given key + /// + /// The key to ignore + public IgnoreKeyValidator(Keys key) + { + _key = key; + } + + /// + /// Checks if the given key has been pressed, and if so, resets the text to its original text. + /// + public override bool ValidateText(ref string text, string oldText) + { + if (Keyboard.GetState().IsKeyDown(_key)) + text = oldText; + + return base.ValidateText(ref text, oldText); + } + } +} \ No newline at end of file