-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
TextField.cs
294 lines (271 loc) · 13.2 KB
/
TextField.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Font;
using MLEM.Graphics;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Textures;
using MLEM.Ui.Style;
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
using TextCopy;
#endif
namespace MLEM.Ui.Elements {
/// <summary>
/// A text field element for use inside of a <see cref="UiSystem"/>.
/// A text field is a selectable element that can be typed in, as well as copied and pasted from.
/// If an on-screen keyboard is required, then this text field will automatically open an on-screen keyboard using <see cref="MlemPlatform.OpenOnScreenKeyboard"/>.
/// This class interally uses MLEM's <see cref="TextInput"/>.
/// </summary>
public class TextField : Element {
/// <inheritdoc cref="TextInput.DefaultRule"/>
public static readonly Rule DefaultRule = (field, add) => TextInput.DefaultRule(field.textInput, add);
/// <inheritdoc cref="TextInput.OnlyLetters"/>
public static readonly Rule OnlyLetters = (field, add) => TextInput.OnlyLetters(field.textInput, add);
/// <inheritdoc cref="TextInput.OnlyNumbers"/>
public static readonly Rule OnlyNumbers = (field, add) => TextInput.OnlyNumbers(field.textInput, add);
/// <inheritdoc cref="TextInput.LettersNumbers"/>
public static readonly Rule LettersNumbers = (field, add) => TextInput.LettersNumbers(field.textInput, add);
/// <inheritdoc cref="TextInput.PathNames"/>
public static readonly Rule PathNames = (field, add) => TextInput.PathNames(field.textInput, add);
/// <inheritdoc cref="TextInput.FileNames"/>
public static readonly Rule FileNames = (field, add) => TextInput.FileNames(field.textInput, add);
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
/// <summary>
/// An event that is raised when an exception is thrown while trying to copy or paste clipboard contents using TextCopy.
/// If no event handlers are added, the exception is ignored.
/// </summary>
public static event Action<Exception> OnCopyPasteException;
#endif
/// <summary>
/// The color that this text field's text should display with
/// </summary>
public StyleProp<Color> TextColor;
/// <summary>
/// The color that the <see cref="PlaceholderText"/> should display with
/// </summary>
public StyleProp<Color> PlaceholderColor;
/// <summary>
/// This text field's texture
/// </summary>
public StyleProp<NinePatch> Texture;
/// <summary>
/// This text field's texture while it is hovered
/// </summary>
public StyleProp<NinePatch> HoveredTexture;
/// <summary>
/// The color that this text field should display with while it is hovered
/// </summary>
public StyleProp<Color> HoveredColor;
/// <summary>
/// The scale that this text field should render text with
/// </summary>
public StyleProp<float> TextScale {
get => this.textScale;
set {
this.textScale = value;
this.textInput.TextScale = value;
}
}
/// <summary>
/// The font that this text field should display text with
/// </summary>
public StyleProp<GenericFont> Font {
get => this.font;
set {
this.font = value;
this.textInput.Font = value;
}
}
/// <summary>
/// The x position that text should start rendering at, based on the x position of this text field.
/// </summary>
public StyleProp<float> TextOffsetX;
/// <summary>
/// The width that the caret should render with, in pixels
/// </summary>
public StyleProp<float> CaretWidth;
/// <inheritdoc cref="TextInput.Text"/>
public string Text => this.textInput.Text;
/// <inheritdoc cref="TextInput.OnTextChange"/>
public TextChanged OnTextChange;
/// <inheritdoc cref="TextInput.InputRule"/>
public Rule InputRule;
/// <inheritdoc cref="TextInput.CaretPos"/>
public int CaretPos {
get => this.textInput.CaretPos;
set => this.textInput.CaretPos = value;
}
/// <inheritdoc cref="TextInput.CaretLine"/>
public int CaretLine => this.textInput.CaretLine;
/// <inheritdoc cref="TextInput.CaretPosInLine"/>
public int CaretPosInLine => this.textInput.CaretPosInLine;
/// <inheritdoc cref="TextInput.MaskingCharacter"/>
public char? MaskingCharacter {
get => this.textInput.MaskingCharacter;
set => this.textInput.MaskingCharacter = value;
}
/// <inheritdoc cref="TextInput.MaximumCharacters"/>
public int? MaximumCharacters {
get => this.textInput.MaximumCharacters;
set => this.textInput.MaximumCharacters = value;
}
/// <inheritdoc cref="TextInput.Multiline"/>
public bool Multiline {
get => this.textInput.Multiline;
set => this.textInput.Multiline = value;
}
#if FNA
/// <inheritdoc />
// we need to make sure that the enter press doesn't get consumed by our press function so that it still works in TextInput
public override bool CanBePressed => base.CanBePressed && !this.IsSelected;
#endif
/// <summary>
/// The text that displays in this text field if <see cref="Text"/> is empty
/// </summary>
public string PlaceholderText;
/// <summary>
/// The title of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileTitle;
/// <summary>
/// The description of the <c>KeyboardInput</c> field on mobile devices and consoles
/// </summary>
public string MobileDescription;
/// <summary>
/// An element that should be pressed (using <see cref="UiControls.PressElement"/>) if <see cref="Keys.Enter"/> is pressed while this text field is active.
/// Note that, for text fields that are <see cref="Multiline"/>, this is ignored.
/// This also occurs once the text input window is successfully closed on a mobile device.
/// </summary>
public Element EnterReceiver;
private readonly TextInput textInput;
private StyleProp<GenericFont> font;
private StyleProp<float> textScale;
/// <summary>
/// Creates a new text field with the given settings
/// </summary>
/// <param name="anchor">The text field's anchor</param>
/// <param name="size">The text field's size</param>
/// <param name="rule">The text field's input rule</param>
/// <param name="font">The font to use for drawing text</param>
/// <param name="text">The text that the text field should contain by default</param>
/// <param name="multiline">Whether the text field should support multi-line editing</param>
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
this.textInput = new TextInput(null, Vector2.Zero, 1
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
, null, s => {
try {
ClipboardService.SetText(s);
} catch (Exception e) {
TextField.OnCopyPasteException?.Invoke(e);
}
}, () => {
try {
return ClipboardService.GetText();
} catch (Exception e) {
TextField.OnCopyPasteException?.Invoke(e);
return null;
}
}
#endif
) {
OnTextChange = (i, s) => this.OnTextChange?.Invoke(this, s),
InputRule = (i, s) => this.InputRule.Invoke(this, s)
};
this.InputRule = rule ?? TextField.DefaultRule;
this.Multiline = multiline;
if (font != null)
this.Font = font;
if (text != null)
this.SetText(text, true);
MlemPlatform.EnsureExists();
this.OnPressed += async e => {
var title = this.MobileTitle ?? this.PlaceholderText;
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
if (result != null) {
this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver);
}
};
this.OnTextInput += (element, key, character) => {
if (this.IsSelectedActive && !this.IsHidden && !this.textInput.OnTextInput(key, character) && key == Keys.Enter && !this.Multiline)
this.EnterReceiver?.Controls?.PressElement(this.EnterReceiver);
};
}
/// <inheritdoc />
public override void SetAreaAndUpdateChildren(RectangleF area) {
base.SetAreaAndUpdateChildren(area);
this.textInput.Size = this.DisplayArea.Size / this.Scale - new Vector2(2 * this.TextOffsetX);
this.textInput.TextScale = this.TextScale;
}
/// <inheritdoc />
public override void Update(GameTime time) {
base.Update(time);
if (this.IsSelectedActive && !this.IsHidden) {
this.textInput.Update(time, this.Input);
#if FNA
// this occurs in OnTextInput outside FNA, where special keys are also counted as text input
if (this.EnterReceiver != null && !this.Multiline && this.Input.TryConsumePressed(Keys.Enter))
this.EnterReceiver.Controls?.PressElement(this.EnterReceiver);
#endif
}
}
/// <inheritdoc />
public override void Draw(GameTime time, SpriteBatch batch, float alpha, SpriteBatchContext context) {
var tex = this.Texture;
var color = Color.White * alpha;
if (this.IsMouseOver) {
tex = this.HoveredTexture.OrDefault(tex);
color = (Color) this.HoveredColor * alpha;
}
batch.Draw(tex, this.DisplayArea, color, this.Scale);
var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale;
var textPos = this.DisplayArea.Location + new Vector2(
this.TextOffsetX * this.Scale,
this.Multiline ? this.TextOffsetX * this.Scale : this.DisplayArea.Height / 2 - lineHeight / 2);
if (this.textInput.Length > 0 || this.IsSelected) {
this.textInput.Draw(batch, textPos, this.Scale, this.IsSelected ? this.CaretWidth : 0, this.TextColor.OrDefault(Color.White) * alpha);
} else if (this.PlaceholderText != null) {
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
}
base.Draw(time, batch, alpha, context);
}
/// <inheritdoc cref="TextInput.SetText"/>
public void SetText(object text, bool removeMismatching = false) {
this.textInput.SetText(text, removeMismatching);
}
/// <inheritdoc cref="TextInput.InsertText"/>
public void InsertText(object text, bool removeMismatching = false) {
this.textInput.InsertText(text, removeMismatching);
}
/// <inheritdoc cref="TextInput.RemoveText"/>
public void RemoveText(int index, int length) {
this.textInput.RemoveText(index, length);
}
/// <inheritdoc />
protected override void InitStyle(UiStyle style) {
base.InitStyle(style);
this.TextScale = this.TextScale.OrStyle(style.TextScale);
this.Font = this.Font.OrStyle(style.Font);
this.Texture = this.Texture.OrStyle(style.TextFieldTexture);
this.HoveredTexture = this.HoveredTexture.OrStyle(style.TextFieldHoveredTexture);
this.HoveredColor = this.HoveredColor.OrStyle(style.TextFieldHoveredColor);
this.TextOffsetX = this.TextOffsetX.OrStyle(style.TextFieldTextOffsetX);
this.CaretWidth = this.CaretWidth.OrStyle(style.TextFieldCaretWidth);
}
/// <summary>
/// A delegate method used for <see cref="TextField.OnTextChange"/>
/// </summary>
/// <param name="field">The text field whose text changed</param>
/// <param name="text">The new text</param>
public delegate void TextChanged(TextField field, string text);
/// <summary>
/// A delegate method used for <see cref="InputRule"/>.
/// It should return whether the given text can be added to the text field.
/// </summary>
/// <param name="field">The text field</param>
/// <param name="textToAdd">The text that is tried to be added</param>
public delegate bool Rule(TextField field, string textToAdd);
}
}