Files
Voile/Voile/Source/UI/Widgets/InputField.cs

289 lines
7.2 KiB
C#

using System.Numerics;
using Voile.Input;
using Voile.Rendering;
using Voile.Resources;
namespace Voile.UI.Widgets;
public class InputField : Widget, ITickableElement
{
enum ActionType { None, Backspace, Left, Right }
public override Rect MinimumSize => _placeholderSize + _padding;
public override string? StyleElementName => nameof(InputField);
public override string[]? StyleModifiers =>
[
_isFocused ? "Focused" : string.Empty
];
/// <summary>
/// Current text value of this input field.
/// </summary>
public string Value
{
get => _input;
set
{
_input = value ?? string.Empty;
_cursor = Math.Clamp(_cursor, 0, _input.Length);
MarkDirty(DirtyFlags.Content);
}
}
public string PlaceholderText { get; set; } = string.Empty;
public bool IsFocused => _isFocused;
public FontSet FontSet { get; set; } = new();
public InputField(string initialText, FontSet fontSet)
{
_input = initialText ?? string.Empty;
FontSet = fontSet;
MarkDirty();
}
public void Tick(float dt, InputSystem input)
{
if (!_isFocused)
return;
HandleRepeatingActions(dt, input);
}
protected override void OnInput(UIInputContext context)
{
HandleFocus(context);
if (!_isFocused)
{
_lastActiveAction = ActionType.None;
_repeatTimer = 0.0f;
return;
}
HandleTextInput(context);
ClampCursor();
}
private void HandleFocus(UIInputContext context)
{
if (!context.MousePressed)
return;
bool isInside = ContainsPoint(context.MousePosition);
_isFocused = isInside;
if (isInside)
context.SetHandled();
}
private void HandleTextInput(UIInputContext context)
{
if (!context.HasCharInput)
return;
char c = (char)context.CharPressed;
if (!char.IsControl(c))
{
_input = _input.Insert(_cursor, c.ToString());
_cursor++;
MarkDirty(DirtyFlags.Content);
}
context.SetHandled();
}
private void HandleRepeatingActions(float dt, InputSystem input)
{
ActionType currentAction = ActionType.None;
if (input.IsKeyboardKeyDown(KeyboardKey.Backspace)) currentAction = ActionType.Backspace;
else if (input.IsKeyboardKeyDown(KeyboardKey.Left)) currentAction = ActionType.Left;
else if (input.IsKeyboardKeyDown(KeyboardKey.Right)) currentAction = ActionType.Right;
if (currentAction == ActionType.None)
{
_lastActiveAction = ActionType.None;
_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(ActionType action)
{
switch (action)
{
case ActionType.Backspace:
if (_cursor > 0 && _input.Length > 0)
{
_input = _input.Remove(_cursor - 1, 1);
_cursor--;
MarkDirty(DirtyFlags.Content);
}
break;
case ActionType.Left:
if (_cursor > 0)
{
_cursor--;
MarkDirty(DirtyFlags.Content);
}
break;
case ActionType.Right:
if (_cursor < _input.Length)
{
_cursor++;
MarkDirty(DirtyFlags.Content);
}
break;
}
}
private void ClampCursor()
{
_cursor = Math.Clamp(_cursor, 0, _input.Length);
}
protected override void OnRender(RenderSystem renderer, Style style)
{
_padding = style.Padding ?? Voile.Size.Zero;
var textColor = style.TextColor ?? Color.Black;
var caretColor = style.BorderColor ?? Color.Black;
var pos = new Vector2(GlobalPosition.X + _padding.Left,
GlobalPosition.Y + _padding.Top);
renderer.SetTransform(pos, Vector2.Zero);
string text = string.IsNullOrEmpty(_input)
? PlaceholderText
: _input;
// TODO: use a placeholder color from the style instead of making it lighter than the original text color.
var placeholderColor = textColor.Lightened(0.5f);
var color = string.IsNullOrEmpty(_input)
? placeholderColor
: textColor;
if (_font.HasValue)
renderer.DrawText(_font, text, color);
if (_isFocused && _blink)
{
float caretX = GetCaretX(_cursor);
renderer.SetTransform(
new Vector2(GlobalPosition.X + _padding.Left + caretX,
GlobalPosition.Y + _padding.Top),
Vector2.Zero);
float h = Math.Max(_textSize.Height, _placeholderSize.Height);
renderer.DrawRectangle(new Vector2(1, h), caretColor);
}
}
protected override void OnUpdate(LayoutContext layoutContext)
{
if (!_font.HasValue)
ResolveFont();
if (!_font.HasValue)
return;
var font = _font.Value;
_textSize = font.Measure(_input);
if (_input.Length == 0)
_placeholderSize = font.Measure(PlaceholderText);
var content = Rect.MaxWidth(_textSize, _placeholderSize);
float width = content.Width + _padding.Left + _padding.Right;
float height = content.Height + _padding.Top + _padding.Bottom;
if (LayoutSize.Width == width && LayoutSize.Height == height)
return;
LayoutSize = new Rect(width, height);
MarkDirty(DirtyFlags.Layout);
}
private float GetCaretX(int index)
{
var span = _input.AsSpan(0, index);
if (!_font.HasValue)
return 0.0f;
return _font.Value.Measure(span).Width;
}
private void ResolveFont()
{
if (_font.HasValue)
return;
var text = string.IsNullOrEmpty(_input) ? PlaceholderText : _input;
foreach (var c in text)
{
if (FontSet.TryGetFontFor(c, out var f))
_font = f;
}
}
private float _repeatTimer = 0.0f;
private ActionType _lastActiveAction = ActionType.None;
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;
private bool _blink = true;
private ResourceRef<Font> _font = ResourceRef<Font>.Empty();
private Size _padding = Voile.Size.Zero;
private Rect _textSize = Rect.Zero;
private Rect _placeholderSize = Rect.Zero;
}