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