mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-02 18:17:09 +02:00
480 lines
14 KiB
C#
480 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using JetBrains.Annotations;
|
|
using Robust.Client.GameObjects.EntitySystems;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Graphics.Drawing;
|
|
using Robust.Client.Input;
|
|
using Robust.Client.Interfaces.Input;
|
|
using Robust.Client.Interfaces.UserInterface;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Robust.Client.UserInterface.Controls
|
|
{
|
|
/// <summary>
|
|
/// Allows the user to input and modify a line of text.
|
|
/// </summary>
|
|
public class LineEdit : Control
|
|
{
|
|
public const string StylePropertyStyleBox = "stylebox";
|
|
public const string StyleClassLineEditNotEditable = "notEditable";
|
|
public const string StylePseudoClassPlaceholder = "placeholder";
|
|
|
|
[NotNull] private string _text = "";
|
|
private bool _editable = true;
|
|
[CanBeNull] private string _placeHolder;
|
|
private int _cursorPosition;
|
|
private float _cursorBlinkTimer;
|
|
private bool _cursorCurrentlyLit;
|
|
private const float BlinkTime = 0.5f;
|
|
|
|
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
|
|
|
|
public LineEdit()
|
|
{
|
|
MouseFilter = MouseFilterMode.Stop;
|
|
CanKeyboardFocus = true;
|
|
KeyboardFocusOnClick = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the LineEdit text gets changed by the input text.
|
|
/// </summary>
|
|
public Func<string, bool> IsValid { get; set; }
|
|
|
|
public AlignMode TextAlign { get; set; }
|
|
|
|
public string Text
|
|
{
|
|
get => _text;
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
value = "";
|
|
}
|
|
|
|
if (!SetText(value))
|
|
{
|
|
return;
|
|
}
|
|
_cursorPosition = 0;
|
|
_updatePseudoClass();
|
|
}
|
|
}
|
|
|
|
public bool Editable
|
|
{
|
|
get => _editable;
|
|
set
|
|
{
|
|
_editable = value;
|
|
if (!_editable)
|
|
{
|
|
AddStyleClass(StyleClassLineEditNotEditable);
|
|
}
|
|
else
|
|
{
|
|
RemoveStyleClass(StyleClassLineEditNotEditable);
|
|
}
|
|
}
|
|
}
|
|
|
|
public string PlaceHolder
|
|
{
|
|
get => _placeHolder;
|
|
set
|
|
{
|
|
_placeHolder = value;
|
|
_updatePseudoClass();
|
|
}
|
|
}
|
|
|
|
public int CursorPos
|
|
{
|
|
set
|
|
{
|
|
if (value < 0)
|
|
{
|
|
value = 0;
|
|
}
|
|
else if (value > _text.Length)
|
|
{
|
|
value = _text.Length;
|
|
}
|
|
_cursorPosition = value;
|
|
}
|
|
}
|
|
|
|
public bool IgnoreNext { get; set; }
|
|
|
|
// TODO:
|
|
// I decided to not implement the entire LineEdit API yet,
|
|
// since most of it won't be used yet (if at all).
|
|
// Feel free to implement wrappers for all the other properties!
|
|
// Future me reporting, thanks past me.
|
|
// Second future me reporting, thanks again.
|
|
// Third future me is here to say thanks.
|
|
// Fourth future me is here to continue the tradition.
|
|
|
|
public void Clear()
|
|
{
|
|
Text = "";
|
|
}
|
|
|
|
public void Select(int from = 0, int to = -1)
|
|
{
|
|
}
|
|
|
|
public void SelectAll()
|
|
{
|
|
}
|
|
|
|
public event Action<LineEditEventArgs> OnTextChanged;
|
|
public event Action<LineEditEventArgs> OnTextEntered;
|
|
|
|
public void InsertAtCursor(string text)
|
|
{
|
|
// Strip newlines.
|
|
var chars = new List<char>(text.Length);
|
|
foreach (var chr in text)
|
|
{
|
|
if (chr == '\n')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
chars.Add(chr);
|
|
}
|
|
|
|
if (chars.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!SetText(_text.Insert(_cursorPosition, new string(chars.ToArray()))))
|
|
{
|
|
return;
|
|
}
|
|
_cursorPosition += chars.Count;
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_updatePseudoClass();
|
|
}
|
|
|
|
protected bool SetText(string newText)
|
|
{
|
|
if (IsValid != null && !IsValid(newText))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_text = newText;
|
|
return true;
|
|
}
|
|
|
|
protected internal override void Draw(DrawingHandleScreen handle)
|
|
{
|
|
var styleBox = _getStyleBox();
|
|
var drawBox = PixelSizeBox;
|
|
var contentBox = styleBox.GetContentBox(drawBox);
|
|
styleBox.Draw(handle, drawBox);
|
|
var font = _getFont();
|
|
var renderedTextColor = _getFontColor();
|
|
|
|
var offsetY = (int) (contentBox.Height - font.GetHeight(UIScale)) / 2;
|
|
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + contentBox.TopLeft;
|
|
|
|
string renderedText;
|
|
|
|
if (IsPlaceHolderVisible)
|
|
{
|
|
renderedText = _placeHolder;
|
|
}
|
|
else
|
|
{
|
|
renderedText = _text;
|
|
}
|
|
|
|
float? actualCursorPosition = null;
|
|
|
|
if (_cursorPosition == 0)
|
|
{
|
|
actualCursorPosition = contentBox.Left;
|
|
}
|
|
|
|
var count = 0;
|
|
foreach (var chr in renderedText)
|
|
{
|
|
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
|
{
|
|
count += 1;
|
|
continue;
|
|
}
|
|
|
|
// Glyph would be outside the bounding box, abort.
|
|
if (baseLine.X + metrics.Width + metrics.BearingX > contentBox.Right)
|
|
{
|
|
break;
|
|
}
|
|
|
|
font.DrawChar(handle, chr, baseLine, UIScale, renderedTextColor);
|
|
baseLine += new Vector2(metrics.Advance, 0);
|
|
count += 1;
|
|
if (count == _cursorPosition)
|
|
{
|
|
actualCursorPosition = baseLine.X;
|
|
}
|
|
}
|
|
|
|
if (_cursorCurrentlyLit && actualCursorPosition.HasValue && HasKeyboardFocus())
|
|
{
|
|
handle.DrawRect(
|
|
new UIBox2(actualCursorPosition.Value, contentBox.Top, actualCursorPosition.Value + 1,
|
|
contentBox.Bottom), Color.White);
|
|
}
|
|
}
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
_cursorBlinkTimer -= args.DeltaSeconds;
|
|
if (_cursorBlinkTimer <= 0)
|
|
{
|
|
_cursorBlinkTimer += BlinkTime;
|
|
_cursorCurrentlyLit = !_cursorCurrentlyLit;
|
|
}
|
|
}
|
|
|
|
protected override Vector2 CalculateMinimumSize()
|
|
{
|
|
var font = _getFont();
|
|
var style = _getStyleBox();
|
|
return new Vector2(0, font.GetHeight(UIScale) / UIScale) + style.MinimumSize / UIScale;
|
|
}
|
|
|
|
protected internal override void TextEntered(GUITextEventArgs args)
|
|
{
|
|
base.TextEntered(args);
|
|
|
|
if (!Editable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IgnoreNext)
|
|
{
|
|
IgnoreNext = false;
|
|
return;
|
|
}
|
|
|
|
if (!SetText(_text.Insert(_cursorPosition, ((char)args.CodePoint).ToString())))
|
|
{
|
|
return;
|
|
}
|
|
_cursorPosition += 1;
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_updatePseudoClass();
|
|
}
|
|
|
|
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
|
{
|
|
base.KeyBindDown(args);
|
|
|
|
if (!args.CanFocus)
|
|
{
|
|
if (!this.HasKeyboardFocus())
|
|
{
|
|
return;
|
|
}
|
|
if (args.Function == EngineKeyFunctions.TextBackspace)
|
|
{
|
|
if (_cursorPosition == 0 || !Editable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_text = _text.Remove(_cursorPosition - 1, 1);
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_cursorPosition -= 1;
|
|
_updatePseudoClass();
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorLeft)
|
|
{
|
|
if (_cursorPosition == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cursorPosition -= 1;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorRight)
|
|
{
|
|
if (_cursorPosition == _text.Length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cursorPosition += 1;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorBegin)
|
|
{
|
|
_cursorPosition = 0;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorEnd)
|
|
{
|
|
_cursorPosition = _text.Length;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextSubmit)
|
|
{
|
|
if (Editable)
|
|
{
|
|
OnTextEntered?.Invoke(new LineEditEventArgs(this, _text));
|
|
}
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextPaste)
|
|
{
|
|
if (Editable)
|
|
{
|
|
var clipboard = IoCManager.Resolve<IClipboardManager>();
|
|
InsertAtCursor(clipboard.GetText());
|
|
}
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextDelete)
|
|
{
|
|
if (_cursorPosition >= _text.Length || !Editable)
|
|
return;
|
|
|
|
_text = _text.Remove(_cursorPosition, 1);
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_updatePseudoClass();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Find closest cursor position under mouse.
|
|
var style = _getStyleBox();
|
|
var contentBox = style.GetContentBox(PixelSizeBox);
|
|
|
|
var clickPosX = args.RelativePosition.X * UIScale;
|
|
|
|
var font = _getFont();
|
|
var index = 0;
|
|
var chrPosX = contentBox.Left;
|
|
var lastChrPostX = contentBox.Left;
|
|
foreach (var chr in _text)
|
|
{
|
|
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
|
{
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (chrPosX > clickPosX)
|
|
{
|
|
break;
|
|
}
|
|
|
|
lastChrPostX = chrPosX;
|
|
chrPosX += metrics.Advance;
|
|
index += 1;
|
|
|
|
if (chrPosX > contentBox.Right)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Distance between the right side of the glyph overlapping the mouse and the mouse.
|
|
var distanceRight = chrPosX - clickPosX;
|
|
// Same but left side.
|
|
var distanceLeft = clickPosX - lastChrPostX;
|
|
// If the mouse is closer to the left of the glyph we lower the index one, so we select before that glyph.
|
|
if (index > 0 && distanceRight > distanceLeft)
|
|
{
|
|
index -= 1;
|
|
}
|
|
|
|
_cursorPosition = index;
|
|
}
|
|
// Reset this so the cursor is always visible immediately after a keybind is pressed.
|
|
_resetCursorBlink();
|
|
}
|
|
|
|
protected internal override void FocusEntered()
|
|
{
|
|
base.FocusEntered();
|
|
|
|
// Reset this so the cursor is always visible immediately after gaining focus..
|
|
_resetCursorBlink();
|
|
}
|
|
|
|
[Pure]
|
|
private Font _getFont()
|
|
{
|
|
if (TryGetStyleProperty("font", out Font font))
|
|
{
|
|
return font;
|
|
}
|
|
|
|
return UserInterfaceManager.ThemeDefaults.DefaultFont;
|
|
}
|
|
|
|
[Pure]
|
|
private StyleBox _getStyleBox()
|
|
{
|
|
if (TryGetStyleProperty(StylePropertyStyleBox, out StyleBox box))
|
|
{
|
|
return box;
|
|
}
|
|
|
|
return UserInterfaceManager.ThemeDefaults.LineEditBox;
|
|
}
|
|
|
|
[Pure]
|
|
private Color _getFontColor()
|
|
{
|
|
if (TryGetStyleProperty("font-color", out Color color))
|
|
{
|
|
return color;
|
|
}
|
|
|
|
return Color.White;
|
|
}
|
|
|
|
private void _updatePseudoClass()
|
|
{
|
|
SetOnlyStylePseudoClass(IsPlaceHolderVisible ? StylePseudoClassPlaceholder : null);
|
|
}
|
|
|
|
public enum AlignMode
|
|
{
|
|
Left = 0,
|
|
Center = 1,
|
|
Right = 2
|
|
}
|
|
|
|
public class LineEditEventArgs : EventArgs
|
|
{
|
|
public LineEdit Control { get; }
|
|
public string Text { get; }
|
|
|
|
public LineEditEventArgs(LineEdit control, string text)
|
|
{
|
|
Control = control;
|
|
Text = text;
|
|
}
|
|
}
|
|
|
|
private void _resetCursorBlink()
|
|
{
|
|
_cursorCurrentlyLit = true;
|
|
_cursorBlinkTimer = BlinkTime;
|
|
}
|
|
}
|
|
}
|