Backspace and direction key echoing in InputField.

This commit is contained in:
2026-05-31 23:01:50 +02:00
parent c7acc70dcd
commit 828ff3561b
4 changed files with 156 additions and 50 deletions

View File

@@ -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; }
}
/// <summary>
/// Represents a UI element that requires a frame-independent update tick loop.
/// </summary>
public interface ITickableElement
{
/// <summary>
/// Excutes unconditionally on every frame engine loop step.
/// </summary>
/// <param name="dt">Elapsed delta frame time in seconds.</param>
/// <param name="input">InputSystem that this tickable element should use to poll input events.</param>
void Tick(float dt, InputSystem input);
}
/// <summary>
/// Represents a UI element that can contain child elements.
/// </summary>

View File

@@ -55,6 +55,8 @@ public class UIInputContext
public bool IsUp => ActionName == "up";
public bool IsDown => ActionName == "down";
public float DeltaTime { get; init; }
/// <summary>
/// Determines if control key is pressed.
/// </summary>

View File

@@ -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))
{
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);
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed)
{
DeltaTime = deltaTime
};
if (PropagateInput(_elements, context))
{
_lastMousePosition = currentMousePosition;
return;
}
}
if (currentMousePosition != _lastMousePosition)
{
// TODO: specify which mouse button is used in the context.
var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
var frameContext = 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, context);
PropagateInput(_elements, frameContext);
return;
_lastMousePosition = currentMousePosition;
}
private void PropagateTick(List<UIElement> elements, float dt)
{
for (int i = 0; i < elements.Count; i++)
{
var element = elements[i];
if (!element.Visible) continue;
if (element is ITickableElement tickable)
{
tickable.Tick(dt, _input);
}
if (element is IParentableElement parentable)
{
PropagateTick(parentable.Children.ToList(), dt);
}
}
}

View File

@@ -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";
if (string.IsNullOrEmpty(currentAction))
{
_lastActiveAction = string.Empty;
_repeatTimer = 0.0f;
return;
}
bool shouldExecute = false;
if (_lastActiveAction != currentAction)
{
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();
context.SetHandled();
}
break;
private void HandleCursorMovement(UIInputContext context)
{
if (context.IsLeft && _cursor > 0)
case "left":
if (_cursor > 0)
{
_cursor--;
MarkDirty();
context.SetHandled();
}
break;
if (context.IsRight && _cursor < _input.Length)
case "right":
if (_cursor < _input.Length)
{
_cursor++;
MarkDirty();
context.SetHandled();
}
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;