diff --git a/Voile/Source/UI/IElement.cs b/Voile/Source/UI/IElement.cs
index fc6d87f..9d4a29f 100644
--- a/Voile/Source/UI/IElement.cs
+++ b/Voile/Source/UI/IElement.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Voile.Input;
using Voile.Rendering;
namespace Voile.UI;
@@ -18,6 +19,19 @@ public interface IElement
public Rect Size { get; set; }
}
+///
+/// Represents a UI element that requires a frame-independent update tick loop.
+///
+public interface ITickableElement
+{
+ ///
+ /// Excutes unconditionally on every frame engine loop step.
+ ///
+ /// Elapsed delta frame time in seconds.
+ /// InputSystem that this tickable element should use to poll input events.
+ void Tick(float dt, InputSystem input);
+}
+
///
/// Represents a UI element that can contain child elements.
///
diff --git a/Voile/Source/UI/UIInputContext.cs b/Voile/Source/UI/UIInputContext.cs
index 6fa9bd3..db65f07 100644
--- a/Voile/Source/UI/UIInputContext.cs
+++ b/Voile/Source/UI/UIInputContext.cs
@@ -55,6 +55,8 @@ public class UIInputContext
public bool IsUp => ActionName == "up";
public bool IsDown => ActionName == "down";
+ public float DeltaTime { get; init; }
+
///
/// Determines if control key is pressed.
///
diff --git a/Voile/Source/UI/UISystem.cs b/Voile/Source/UI/UISystem.cs
index a20cf72..1d739a6 100644
--- a/Voile/Source/UI/UISystem.cs
+++ b/Voile/Source/UI/UISystem.cs
@@ -39,14 +39,13 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
public void Update(double deltaTime)
{
- // HandleInput();
+ float dt = (float)deltaTime;
+ PropagateTick(_elements, dt);
+ HandleInput((float)deltaTime);
}
public void Render(RenderSystem renderer)
{
- // Update elements each time UI system is rendered.
- HandleInput();
-
foreach (var element in _elements)
{
if (element is not IUpdatableElement updatable) continue;
@@ -104,54 +103,90 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
}
}
- private void HandleInput()
+ private void HandleInput(float deltaTime)
{
int charPressed = _input.GetCharPressed();
Vector2 mousePos = _input.GetMousePosition();
-
Vector2 currentMousePosition = _input.GetMousePosition();
+ bool inputHandled = false;
+ // 1. Process Actions (Pressed or Held)
foreach (var (actionName, mappings) in InputSystem.InputMappings)
{
foreach (var action in mappings)
{
- if (action.IsPressed(_input))
+ bool isPressed = action.IsPressed(_input);
+ bool isDown = action.IsDown(_input);
+
+ if (isPressed)
{
- // TODO: specify which mouse button is used in the context.
var context = new UIInputContext(action, mousePos, actionName, charPressed)
{
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
+ DeltaTime = deltaTime
};
if (PropagateInput(_elements, context))
- return;
+ {
+ inputHandled = true;
+ break;
+ }
}
}
+ if (inputHandled) break;
+ }
+
+ if (inputHandled)
+ {
+ _lastMousePosition = currentMousePosition;
+ return;
}
if (charPressed != 0)
{
- var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed);
- PropagateInput(_elements, context);
- return;
+ var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed)
+ {
+ DeltaTime = deltaTime
+ };
+ if (PropagateInput(_elements, context))
+ {
+ _lastMousePosition = currentMousePosition;
+ return;
+ }
}
-
- if (currentMousePosition != _lastMousePosition)
+ var frameContext = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
{
- // TODO: specify which mouse button is used in the context.
- var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
+ MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
+ MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
+ MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
+ DeltaTime = deltaTime
+ };
+
+ PropagateInput(_elements, frameContext);
+
+ _lastMousePosition = currentMousePosition;
+ }
+
+ private void PropagateTick(List elements, float dt)
+ {
+ for (int i = 0; i < elements.Count; i++)
+ {
+ var element = elements[i];
+
+ if (!element.Visible) continue;
+
+ if (element is ITickableElement tickable)
{
- MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
- MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
- MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
- };
+ tickable.Tick(dt, _input);
+ }
- PropagateInput(_elements, context);
-
- return;
+ if (element is IParentableElement parentable)
+ {
+ PropagateTick(parentable.Children.ToList(), dt);
+ }
}
}
diff --git a/Voile/Source/UI/Widgets/InputField.cs b/Voile/Source/UI/Widgets/InputField.cs
index 4d396ce..536ae42 100644
--- a/Voile/Source/UI/Widgets/InputField.cs
+++ b/Voile/Source/UI/Widgets/InputField.cs
@@ -1,10 +1,11 @@
using System.Numerics;
+using Voile.Input;
using Voile.Rendering;
using Voile.Resources;
namespace Voile.UI.Widgets;
-public class InputField : Widget
+public class InputField : Widget, ITickableElement
{
public override Rect MinimumSize => _placeholderSize + _padding;
public override string? StyleElementName => nameof(InputField);
@@ -43,20 +44,30 @@ public class InputField : Widget
Update();
}
+ public void Tick(float dt, InputSystem input)
+ {
+ if (!_isFocused)
+ return;
+
+ HandleRepeatingActions(dt, input);
+ }
+
protected override void OnInput(UIInputContext context)
{
HandleFocus(context);
if (!_isFocused)
+ {
+ _lastActiveAction = string.Empty;
+ _repeatTimer = 0.0f;
return;
+ }
HandleTextInput(context);
- HandleBackspace(context);
- HandleCursorMovement(context);
-
ClampCursor();
}
+
private void HandleFocus(UIInputContext context)
{
if (!context.MousePressed)
@@ -87,35 +98,74 @@ public class InputField : Widget
context.SetHandled();
}
- private void HandleBackspace(UIInputContext context)
+ private void HandleRepeatingActions(float dt, InputSystem input)
{
- if (!context.IsBackspace)
- return;
+ string currentAction = string.Empty;
- if (_cursor <= 0 || _input.Length == 0)
- return;
+ if (input.IsKeyboardKeyDown(KeyboardKey.Backspace)) currentAction = "backspace";
+ else if (input.IsKeyboardKeyDown(KeyboardKey.Left)) currentAction = "left";
+ else if (input.IsKeyboardKeyDown(KeyboardKey.Right)) currentAction = "right";
- _input = _input.Remove(_cursor - 1, 1);
- _cursor--;
- MarkDirty();
-
- context.SetHandled();
- }
-
- private void HandleCursorMovement(UIInputContext context)
- {
- if (context.IsLeft && _cursor > 0)
+ if (string.IsNullOrEmpty(currentAction))
{
- _cursor--;
- MarkDirty();
- context.SetHandled();
+ _lastActiveAction = string.Empty;
+ _repeatTimer = 0.0f;
+ return;
}
- if (context.IsRight && _cursor < _input.Length)
+ bool shouldExecute = false;
+
+ if (_lastActiveAction != currentAction)
{
- _cursor++;
- MarkDirty();
- context.SetHandled();
+ shouldExecute = true;
+ _repeatTimer = 0.0f;
+ _lastActiveAction = currentAction;
+ }
+ else
+ {
+ _repeatTimer += dt;
+
+ if (_repeatTimer >= INITIAL_DELAY)
+ {
+ shouldExecute = true;
+ _repeatTimer = INITIAL_DELAY - REPEAT_RATE;
+ }
+ }
+
+ if (shouldExecute)
+ {
+ ExecuteAction(currentAction);
+ }
+ }
+
+ private void ExecuteAction(string actionName)
+ {
+ switch (actionName)
+ {
+ case "backspace":
+ if (_cursor > 0 && _input.Length > 0)
+ {
+ _input = _input.Remove(_cursor - 1, 1);
+ _cursor--;
+ MarkDirty();
+ }
+ break;
+
+ case "left":
+ if (_cursor > 0)
+ {
+ _cursor--;
+ MarkDirty();
+ }
+ break;
+
+ case "right":
+ if (_cursor < _input.Length)
+ {
+ _cursor++;
+ MarkDirty();
+ }
+ break;
}
}
@@ -215,7 +265,12 @@ public class InputField : Widget
return _suitableFont.Value.Measure(text).Width;
}
- private string _input = "";
+ private float _repeatTimer = 0.0f;
+ private string _lastActiveAction = string.Empty;
+ private const float INITIAL_DELAY = 0.5f;
+ private const float REPEAT_RATE = 0.05f;
+
+ private string _input = string.Empty;
private bool _isFocused;
private int _cursor;