479 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
} |