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;