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 System.Numerics;
using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
namespace Voile.UI; namespace Voile.UI;
@@ -18,6 +19,19 @@ public interface IElement
public Rect Size { get; set; } 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> /// <summary>
/// Represents a UI element that can contain child elements. /// Represents a UI element that can contain child elements.
/// </summary> /// </summary>

View File

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

View File

@@ -39,14 +39,13 @@ public class UISystem : IUpdatableSystem, IRenderableSystem, IReloadableSystem
public void Update(double deltaTime) public void Update(double deltaTime)
{ {
// HandleInput(); float dt = (float)deltaTime;
PropagateTick(_elements, dt);
HandleInput((float)deltaTime);
} }
public void Render(RenderSystem renderer) public void Render(RenderSystem renderer)
{ {
// Update elements each time UI system is rendered.
HandleInput();
foreach (var element in _elements) foreach (var element in _elements)
{ {
if (element is not IUpdatableElement updatable) continue; 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(); int charPressed = _input.GetCharPressed();
Vector2 mousePos = _input.GetMousePosition(); Vector2 mousePos = _input.GetMousePosition();
Vector2 currentMousePosition = _input.GetMousePosition(); Vector2 currentMousePosition = _input.GetMousePosition();
bool inputHandled = false;
// 1. Process Actions (Pressed or Held)
foreach (var (actionName, mappings) in InputSystem.InputMappings) foreach (var (actionName, mappings) in InputSystem.InputMappings)
{ {
foreach (var action in mappings) 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) var context = new UIInputContext(action, mousePos, actionName, charPressed)
{ {
MouseDown = _input.IsMouseButtonDown(MouseButton.Left), MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left), MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left), MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
DeltaTime = deltaTime
}; };
if (PropagateInput(_elements, context)) if (PropagateInput(_elements, context))
{
inputHandled = true;
break;
}
}
}
if (inputHandled) break;
}
if (inputHandled)
{
_lastMousePosition = currentMousePosition;
return; return;
} }
}
}
if (charPressed != 0) if (charPressed != 0)
{ {
var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed); var context = new UIInputContext(new KeyInputAction(KeyboardKey.Null), mousePos, "", charPressed)
PropagateInput(_elements, context); {
DeltaTime = deltaTime
};
if (PropagateInput(_elements, context))
{
_lastMousePosition = currentMousePosition;
return; return;
} }
}
var frameContext = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
if (currentMousePosition != _lastMousePosition)
{
// 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), MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left), MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
MousePressed = _input.IsMouseButtonPressed(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 System.Numerics;
using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
using Voile.Resources; using Voile.Resources;
namespace Voile.UI.Widgets; namespace Voile.UI.Widgets;
public class InputField : Widget public class InputField : Widget, ITickableElement
{ {
public override Rect MinimumSize => _placeholderSize + _padding; public override Rect MinimumSize => _placeholderSize + _padding;
public override string? StyleElementName => nameof(InputField); public override string? StyleElementName => nameof(InputField);
@@ -43,20 +44,30 @@ public class InputField : Widget
Update(); Update();
} }
public void Tick(float dt, InputSystem input)
{
if (!_isFocused)
return;
HandleRepeatingActions(dt, input);
}
protected override void OnInput(UIInputContext context) protected override void OnInput(UIInputContext context)
{ {
HandleFocus(context); HandleFocus(context);
if (!_isFocused) if (!_isFocused)
{
_lastActiveAction = string.Empty;
_repeatTimer = 0.0f;
return; return;
}
HandleTextInput(context); HandleTextInput(context);
HandleBackspace(context);
HandleCursorMovement(context);
ClampCursor(); ClampCursor();
} }
private void HandleFocus(UIInputContext context) private void HandleFocus(UIInputContext context)
{ {
if (!context.MousePressed) if (!context.MousePressed)
@@ -87,35 +98,74 @@ public class InputField : Widget
context.SetHandled(); context.SetHandled();
} }
private void HandleBackspace(UIInputContext context) private void HandleRepeatingActions(float dt, InputSystem input)
{ {
if (!context.IsBackspace) string currentAction = string.Empty;
return;
if (_cursor <= 0 || _input.Length == 0) if (input.IsKeyboardKeyDown(KeyboardKey.Backspace)) currentAction = "backspace";
return; 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); _input = _input.Remove(_cursor - 1, 1);
_cursor--; _cursor--;
MarkDirty(); MarkDirty();
context.SetHandled();
} }
break;
private void HandleCursorMovement(UIInputContext context) case "left":
{ if (_cursor > 0)
if (context.IsLeft && _cursor > 0)
{ {
_cursor--; _cursor--;
MarkDirty(); MarkDirty();
context.SetHandled();
} }
break;
if (context.IsRight && _cursor < _input.Length) case "right":
if (_cursor < _input.Length)
{ {
_cursor++; _cursor++;
MarkDirty(); MarkDirty();
context.SetHandled(); }
break;
} }
} }
@@ -215,7 +265,12 @@ public class InputField : Widget
return _suitableFont.Value.Measure(text).Width; 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 bool _isFocused;
private int _cursor; private int _cursor;