Backspace and direction key echoing in InputField.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))
|
||||||
return;
|
{
|
||||||
|
inputHandled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (inputHandled) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputHandled)
|
||||||
|
{
|
||||||
|
_lastMousePosition = currentMousePosition;
|
||||||
|
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);
|
{
|
||||||
return;
|
DeltaTime = deltaTime
|
||||||
|
};
|
||||||
|
if (PropagateInput(_elements, context))
|
||||||
|
{
|
||||||
|
_lastMousePosition = currentMousePosition;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var frameContext = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
||||||
if (currentMousePosition != _lastMousePosition)
|
|
||||||
{
|
{
|
||||||
// TODO: specify which mouse button is used in the context.
|
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
||||||
var context = new UIInputContext(new MouseInputAction(MouseButton.Left), mousePos, "", charPressed)
|
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
||||||
|
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
||||||
|
DeltaTime = deltaTime
|
||||||
|
};
|
||||||
|
|
||||||
|
PropagateInput(_elements, frameContext);
|
||||||
|
|
||||||
|
_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)
|
||||||
{
|
{
|
||||||
MouseDown = _input.IsMouseButtonDown(MouseButton.Left),
|
tickable.Tick(dt, _input);
|
||||||
MouseReleased = _input.IsMouseButtonReleased(MouseButton.Left),
|
}
|
||||||
MousePressed = _input.IsMouseButtonPressed(MouseButton.Left),
|
|
||||||
};
|
|
||||||
|
|
||||||
PropagateInput(_elements, context);
|
if (element is IParentableElement parentable)
|
||||||
|
{
|
||||||
return;
|
PropagateTick(parentable.Children.ToList(), dt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
_input = _input.Remove(_cursor - 1, 1);
|
if (string.IsNullOrEmpty(currentAction))
|
||||||
_cursor--;
|
|
||||||
MarkDirty();
|
|
||||||
|
|
||||||
context.SetHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCursorMovement(UIInputContext context)
|
|
||||||
{
|
|
||||||
if (context.IsLeft && _cursor > 0)
|
|
||||||
{
|
{
|
||||||
_cursor--;
|
_lastActiveAction = string.Empty;
|
||||||
MarkDirty();
|
_repeatTimer = 0.0f;
|
||||||
context.SetHandled();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.IsRight && _cursor < _input.Length)
|
bool shouldExecute = false;
|
||||||
|
|
||||||
|
if (_lastActiveAction != currentAction)
|
||||||
{
|
{
|
||||||
_cursor++;
|
shouldExecute = true;
|
||||||
MarkDirty();
|
_repeatTimer = 0.0f;
|
||||||
context.SetHandled();
|
_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;
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user