Files
barotrauma-localmods/Quick Interactions/CSharp/Client/CrabUI/Components/CUITextInput.cs

479 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using EventInput;
using System.Windows;
namespace QICrabUI
{
/// <summary>
/// Text input
/// </summary>
public class CUITextInput : CUIComponent, IKeyboardSubscriber
{
#region IKeyboardSubscriber
private Keys pressedKey;
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveSpecialInput(Keys key)
{
try
{
pressedKey = key;
if (key == Keys.Back) Back();
if (key == Keys.Delete) Delete();
if (key == Keys.Left) MoveLeft();
if (key == Keys.Right) MoveRight();
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveTextInput(string text)
{
try
{
CutSelection();
Text = Text.Insert(CaretPos, text);
CaretPos = CaretPos + 1;
OnTextAdded?.Invoke(text);
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CUI.Log($"ReceiveTextInput {text}");
}
catch (Exception e)
{
CUI.Log(e);
}
}
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveCommandInput(char command)
{
try
{
if (pressedKey == Keys.A) SelectAll();
if (pressedKey == Keys.C) Copy();
if (pressedKey == Keys.V) Paste();
}
catch (Exception e)
{
CUI.Warning(e);
}
//CUI.Log($"ReceiveCommandInput {command}");
}
//Alt+tab?
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public void ReceiveEditingInput(string text, int start, int length)
{
//CUI.Log($"ReceiveEditingInput {text} {start} {length}");
}
//TODO mb should lose focus here
/// <summary>
/// From IKeyboardSubscriber, don't use it directly
/// </summary>
public bool Selected { get; set; }
#endregion
#region Commands
public void SelectAll() => Select(0, Text.Length);
public void Copy()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
Clipboard.SetText(Text.SubstringSafe(Selection.Start, Selection.End));
}
public void Paste()
{
ReceiveTextInput(Clipboard.GetText());
}
public void AddText(string text) => ReceiveTextInput(text);
public void MoveLeft()
{
CaretPos--;
Selection = IntRange.Zero;
}
public void MoveRight()
{
CaretPos++;
Selection = IntRange.Zero;
}
// //TODO
// public void SelectLeft()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos - 1, CaretPos);
// else Selection = new IntRange(Selection.Start - 1, Selection.End);
// }
// //TODO
// public void SelectRight()
// {
// if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos, CaretPos + 1);
// else Selection = new IntRange(Selection.Start, Selection.End + 1);
// }
public void Back()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos - 1);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos);
Text = s1 + s2;
CaretPos = oldState.CaretPos - 1;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
}
public void Delete()
{
TextInputState oldState = State;
if (!Selection.IsEmpty) CutSelection();
else
{
string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos);
string s2 = oldState.Text.SubstringSafe(oldState.CaretPos + 1);
Text = s1 + s2;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
//CaretPos = oldState.CaretPos;
}
}
public void CutSelection()
{
if (Selection.IsEmpty) return;
selectionHandle.Grabbed = false;
string s1 = Text.SubstringSafe(0, Selection.Start);
string s2 = Text.SubstringSafe(Selection.End);
Text = s1 + s2;
CaretPos = Selection.Start;
Selection = IntRange.Zero;
if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
}
internal int SetCaretPos(Vector2 v)
{
int newCaretPos = TextComponent.CaretIndex(v.X);
CaretPos = newCaretPos;
return newCaretPos;
}
#endregion
internal class SelectionHandle
{
public bool Grabbed;
public int lastSelectedPos;
}
internal SelectionHandle selectionHandle = new SelectionHandle();
internal record struct TextInputState(string Text, IntRange Selection, int CaretPos)
{
public string Text { get; init; } = Text ?? "";
}
private TextInputState state; internal TextInputState State
{
get => state;
set
{
state = ValidateState(value);
ApplyState(state);
}
}
internal TextInputState ValidateState(TextInputState state)
{
//return state with { CaretPos = state.CaretPos.Fit(0, state.Text.Length - 1) };
string newText = state.Text;
IntRange newSelection = new IntRange(
state.Selection.Start.Fit(0, newText.Length),
state.Selection.End.Fit(0, newText.Length)
);
int newCaretPos = state.CaretPos.Fit(0, newText.Length);
return new TextInputState(newText, newSelection, newCaretPos);
}
internal void ApplyState(TextInputState state)
{
TextComponent.Text = state.Text;
SelectionOverlay.Visible = !state.Selection.IsEmpty;
CaretIndicatorVisible = Focused && !SelectionOverlay.Visible;
if (!state.Selection.IsEmpty)
{
SelectionOverlay.Absolute = SelectionOverlay.Absolute with
{
Left = TextComponent.CaretPos(state.Selection.Start),
Width = TextComponent.CaretPos(state.Selection.End) - TextComponent.CaretPos(state.Selection.Start),
};
}
CaretIndicator.Absolute = CaretIndicator.Absolute with
{
Left = TextComponent.CaretPos(state.CaretPos),
};
}
private bool valid = true; public bool Valid
{
get => valid;
set
{
if (valid == value) return;
valid = value;
UpdateBorderColor();
}
}
public Type VatidationType { get; set; }
public bool IsValidText(string text)
{
if (VatidationType == null) return true;
if (VatidationType == typeof(string)) return true;
if (VatidationType == typeof(Color)) return true;
if (VatidationType == typeof(bool)) return bool.TryParse(text, out _);
if (VatidationType == typeof(int)) return int.TryParse(text, out _);
if (VatidationType == typeof(float)) return float.TryParse(text, out _);
if (VatidationType == typeof(double)) return double.TryParse(text, out _);
return false;
}
//TODO this is cringe
// public override void Consume(object o)
// {
// string value = (string)o;
// State = new TextInputState(value, State.Selection, State.CaretPos);
// Valid = IsValidText(value);
// }
internal CUITextBlock TextComponent;
public string Text
{
get => State.Text;
set
{
if (Disabled) return;
State = new TextInputState(value, State.Selection, State.CaretPos);
bool isvalid = IsValidText(value);
if (isvalid)
{
OnTextChanged?.Invoke(State.Text);
}
Valid = isvalid;
}
}
internal CUIComponent SelectionOverlay;
public IntRange Selection
{
get => State.Selection;
set => State = new TextInputState(State.Text, value, State.CaretPos);
}
public void Select(int start, int end) => Selection = new IntRange(start, end);
public bool CaretIndicatorVisible { get; set; }
public double CaretBlinkInterval { get; set; } = 0.5;
internal CUIComponent CaretIndicator;
public int CaretPos
{
get => State.CaretPos;
set => State = new TextInputState(State.Text, State.Selection, value);
}
//TODO
//[CUISerializable] public bool PreventOverflow { get; set; } = false;
public void UpdateBorderColor()
{
if (Valid)
{
if (Focused)
{
Style["Border"] = "CUIPalette.Input.Focused";
}
else
{
Style["Border"] = "CUIPalette.Input.Border";
}
}
else
{
Style["Border"] = "CUIPalette.Input.Invalid";
}
}
[CUISerializable]
public float TextScale
{
get => TextComponent?.TextScale ?? 0;
set => TextComponent.TextScale = value;
}
public Color TextColor
{
get => TextComponent?.TextColor ?? Color.White;
set
{
if (TextComponent != null)
{
TextComponent.TextColor = value;
}
}
}
public event Action<string> OnTextChanged;
public Action<string> AddOnTextChanged { set => OnTextChanged += value; }
public event Action<string> OnTextAdded;
public Action<string> AddOnTextAdded { set => OnTextAdded += value; }
public override void Draw(SpriteBatch spriteBatch)
{
if (Focused)
{
CaretIndicator.Visible = CaretIndicatorVisible && Timing.TotalTime % CaretBlinkInterval > CaretBlinkInterval / 2;
}
base.Draw(spriteBatch);
}
public CUITextInput(string text) : this()
{
Text = text;
}
public CUITextInput() : base()
{
AbsoluteMin = new CUINullRect(w: 50, h: 22);
FitContent = new CUIBool2(true, true);
Focusable = true;
Border.Thickness = 2;
HideChildrenOutsideFrame = true;
ConsumeMouseClicks = true;
ConsumeDragAndDrop = true;
ConsumeSwipe = true;
BreakSerialization = true;
this["TextComponent"] = TextComponent = new CUITextBlock()
{
Text = "",
Relative = new CUINullRect(0, 0, 1, 1),
TextAlign = CUIAnchor.CenterLeft,
Style = new CUIStyle(){
{"Padding", "[2,2]"},
{"TextColor", "CUIPalette.Input.Text"},
},
};
this["SelectionOverlay"] = SelectionOverlay = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Selection"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(h: 1),
Ghost = new CUIBool2(true, true),
IgnoreParentVisibility = true,
};
this["CaretIndicator"] = CaretIndicator = new CUIComponent()
{
Style = new CUIStyle(){
{"BackgroundColor", "CUIPalette.Input.Caret"},
{"Border", "Transparent"},
},
Relative = new CUINullRect(y: 0.1f, h: 0.8f),
Absolute = new CUINullRect(w: 1),
Ghost = new CUIBool2(true, true),
Visible = false,
IgnoreParentVisibility = true,
};
OnConsume += (o) =>
{
string value = o.ToString();
State = new TextInputState(value, State.Selection, State.CaretPos);
Valid = IsValidText(value);
};
OnFocus += () =>
{
UpdateBorderColor();
CaretIndicator.Visible = true;
};
OnFocusLost += () =>
{
UpdateBorderColor();
Selection = IntRange.Zero;
CaretIndicator.Visible = false;
};
OnMouseDown += (e) =>
{
int newCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = IntRange.Zero;
selectionHandle.lastSelectedPos = newCaretPos;
selectionHandle.Grabbed = true;
};
OnMouseMove += (e) =>
{
if (selectionHandle.Grabbed)
{
int nextCaretPos = SetCaretPos(e.MousePosition - Real.Position);
Selection = new IntRange(selectionHandle.lastSelectedPos, nextCaretPos);
}
};
OnDClick += (e) => SelectAll();
if (CUI.Main is not null)
{
CUI.Main.Global.OnMouseUp += (e) => selectionHandle.Grabbed = false;
}
}
}
}