289 lines
7.2 KiB
C#
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;
|
|
} |