CrabUI
Loading...
Searching...
No Matches
CUITextInput.cs
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5
6using Barotrauma;
7using Microsoft.Xna.Framework;
8using Microsoft.Xna.Framework.Input;
9using Microsoft.Xna.Framework.Graphics;
10using EventInput;
11using System.Windows;
12
13namespace CrabUI
14{
15
16 /// <summary>
17 /// Text input
18 /// </summary>
19 public class CUITextInput : CUIComponent, IKeyboardSubscriber
20 {
21
22 #region IKeyboardSubscriber
23
24 private Keys pressedKey;
25
26 /// <summary>
27 /// From IKeyboardSubscriber, don't use it directly
28 /// </summary>
29 public void ReceiveSpecialInput(Keys key)
30 {
31 try
32 {
33 pressedKey = key;
34 if (key == Keys.Back) Back();
35 if (key == Keys.Delete) Delete();
36 if (key == Keys.Left) MoveLeft();
37 if (key == Keys.Right) MoveRight();
38 }
39 catch (Exception e)
40 {
41 CUI.Warning(e);
42 }
43 }
44 /// <summary>
45 /// From IKeyboardSubscriber, don't use it directly
46 /// </summary>
47 public void ReceiveTextInput(char inputChar) => ReceiveTextInput(inputChar.ToString());
48 /// <summary>
49 /// From IKeyboardSubscriber, don't use it directly
50 /// </summary>
51 public void ReceiveTextInput(string text)
52 {
53 try
54 {
55 CutSelection();
56 Text = Text.Insert(CaretPos, text);
57 CaretPos = CaretPos + 1;
58 OnTextAdded?.Invoke(text);
59 if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
60 //CUI.Log($"ReceiveTextInput {text}");
61 }
62 catch (Exception e)
63 {
64 CUI.Log(e);
65 }
66
67 }
68 /// <summary>
69 /// From IKeyboardSubscriber, don't use it directly
70 /// </summary>
71 public void ReceiveCommandInput(char command)
72 {
73 try
74 {
75 if (pressedKey == Keys.A) SelectAll();
76 if (pressedKey == Keys.C) Copy();
77 if (pressedKey == Keys.V) Paste();
78 }
79 catch (Exception e)
80 {
81 CUI.Warning(e);
82 }
83 //CUI.Log($"ReceiveCommandInput {command}");
84 }
85
86 //Alt+tab?
87 /// <summary>
88 /// From IKeyboardSubscriber, don't use it directly
89 /// </summary>
90 public void ReceiveEditingInput(string text, int start, int length)
91 {
92 //CUI.Log($"ReceiveEditingInput {text} {start} {length}");
93 }
94
95 //TODO mb should lose focus here
96 /// <summary>
97 /// From IKeyboardSubscriber, don't use it directly
98 /// </summary>
99 public bool Selected { get; set; }
100 #endregion
101
102 #region Commands
103 public void SelectAll() => Select(0, Text.Length);
104
105 public void Copy()
106 {
107 if (Selection.IsEmpty) return;
108 selectionHandle.Grabbed = false;
109 Clipboard.SetText(Text.SubstringSafe(Selection.Start, Selection.End));
110 }
111 public void Paste()
112 {
113 ReceiveTextInput(Clipboard.GetText());
114 }
115
116 public void AddText(string text) => ReceiveTextInput(text);
117 public void MoveLeft()
118 {
119 CaretPos--;
120 Selection = IntRange.Zero;
121 }
122 public void MoveRight()
123 {
124 CaretPos++;
125 Selection = IntRange.Zero;
126 }
127
128 // //TODO
129 // public void SelectLeft()
130 // {
131 // if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos - 1, CaretPos);
132 // else Selection = new IntRange(Selection.Start - 1, Selection.End);
133 // }
134 // //TODO
135 // public void SelectRight()
136 // {
137 // if (Selection == IntRange.Zero) Selection = new IntRange(CaretPos, CaretPos + 1);
138 // else Selection = new IntRange(Selection.Start, Selection.End + 1);
139 // }
140
141 public void Back()
142 {
143 TextInputState oldState = State;
144 if (!Selection.IsEmpty) CutSelection();
145 else
146 {
147 string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos - 1);
148 string s2 = oldState.Text.SubstringSafe(oldState.CaretPos);
149 Text = s1 + s2;
150 CaretPos = oldState.CaretPos - 1;
151 if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
152 }
153 }
154
155 public void Delete()
156 {
157 TextInputState oldState = State;
158 if (!Selection.IsEmpty) CutSelection();
159 else
160 {
161 string s1 = oldState.Text.SubstringSafe(0, oldState.CaretPos);
162 string s2 = oldState.Text.SubstringSafe(oldState.CaretPos + 1);
163 Text = s1 + s2;
164 if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
165 //CaretPos = oldState.CaretPos;
166 }
167 }
168
169 public void CutSelection()
170 {
171 if (Selection.IsEmpty) return;
172 selectionHandle.Grabbed = false;
173 string s1 = Text.SubstringSafe(0, Selection.Start);
174 string s2 = Text.SubstringSafe(Selection.End);
175 Text = s1 + s2;
176 CaretPos = Selection.Start;
177 Selection = IntRange.Zero;
178 if (Command != null) DispatchUp(new CUICommand(Command, State.Text));
179 }
180
181 internal int SetCaretPos(Vector2 v)
182 {
183 int newCaretPos = TextComponent.CaretIndex(v.X);
184 CaretPos = newCaretPos;
185 return newCaretPos;
186 }
187
188 #endregion
189
190 internal class SelectionHandle
191 {
192 public bool Grabbed;
193 public int lastSelectedPos;
194 }
195 internal SelectionHandle selectionHandle = new SelectionHandle();
196
197 internal record struct TextInputState(string Text, IntRange Selection, int CaretPos)
198 {
199 public string Text { get; init; } = Text ?? "";
200 }
201 private TextInputState state; internal TextInputState State
202 {
203 get => state;
204 set
205 {
206 state = ValidateState(value);
207 ApplyState(state);
208 }
209 }
210
211 internal TextInputState ValidateState(TextInputState state)
212 {
213 //return state with { CaretPos = state.CaretPos.Fit(0, state.Text.Length - 1) };
214
215 string newText = state.Text;
216
217 IntRange newSelection = new IntRange(
218 state.Selection.Start.Fit(0, newText.Length),
219 state.Selection.End.Fit(0, newText.Length)
220 );
221
222 int newCaretPos = state.CaretPos.Fit(0, newText.Length);
223
224 return new TextInputState(newText, newSelection, newCaretPos);
225 }
226
227 internal void ApplyState(TextInputState state)
228 {
229 TextComponent.Text = state.Text;
230
231 SelectionOverlay.Visible = !state.Selection.IsEmpty;
232 CaretIndicatorVisible = Focused && !SelectionOverlay.Visible;
233
234 if (!state.Selection.IsEmpty)
235 {
236 SelectionOverlay.Absolute = SelectionOverlay.Absolute with
237 {
238 Left = TextComponent.CaretPos(state.Selection.Start),
239 Width = TextComponent.CaretPos(state.Selection.End) - TextComponent.CaretPos(state.Selection.Start),
240 };
241 }
242
243 CaretIndicator.Absolute = CaretIndicator.Absolute with
244 {
245 Left = TextComponent.CaretPos(state.CaretPos),
246 };
247 }
248
249
250
251 private bool valid = true; public bool Valid
252 {
253 get => valid;
254 set
255 {
256 if (valid == value) return;
257 valid = value;
258 UpdateBorderColor();
259 }
260 }
261
262 public Type VatidationType { get; set; }
263 public bool IsValidText(string text)
264 {
265 if (VatidationType == null) return true;
266
267 if (VatidationType == typeof(string)) return true;
268 if (VatidationType == typeof(Color)) return true;
269 if (VatidationType == typeof(bool)) return bool.TryParse(text, out _);
270 if (VatidationType == typeof(int)) return int.TryParse(text, out _);
271 if (VatidationType == typeof(float)) return float.TryParse(text, out _);
272 if (VatidationType == typeof(double)) return double.TryParse(text, out _);
273
274 return false;
275 }
276
277 //TODO this is cringe
278 // public override void Consume(object o)
279 // {
280 // string value = (string)o;
281 // State = new TextInputState(value, State.Selection, State.CaretPos);
282 // Valid = IsValidText(value);
283 // }
284
285 internal CUITextBlock TextComponent;
286 public string Text
287 {
288 get => State.Text;
289 set
290 {
291 if (Disabled) return;
292
293 State = new TextInputState(value, State.Selection, State.CaretPos);
294
295 bool isvalid = IsValidText(value);
296 if (isvalid)
297 {
298 OnTextChanged?.Invoke(State.Text);
299 }
300 Valid = isvalid;
301 }
302 }
303
304 internal CUIComponent SelectionOverlay;
305 public IntRange Selection
306 {
307 get => State.Selection;
308 set => State = new TextInputState(State.Text, value, State.CaretPos);
309 }
310 public void Select(int start, int end) => Selection = new IntRange(start, end);
311
312
313 public bool CaretIndicatorVisible { get; set; }
314 public double CaretBlinkInterval { get; set; } = 0.5;
315 internal CUIComponent CaretIndicator;
316 public int CaretPos
317 {
318 get => State.CaretPos;
319 set => State = new TextInputState(State.Text, State.Selection, value);
320 }
321
322
323 //TODO
324 //[CUISerializable] public bool PreventOverflow { get; set; } = false;
325
326 public void UpdateBorderColor()
327 {
328 if (Valid)
329 {
330 if (Focused)
331 {
332 Style["Border"] = "CUIPalette.Input.Focused";
333 }
334 else
335 {
336 Style["Border"] = "CUIPalette.Input.Border";
337 }
338 }
339 else
340 {
341 Style["Border"] = "CUIPalette.Input.Invalid";
342 }
343 }
344 [CUISerializable]
345 public float TextScale
346 {
347 get => TextComponent?.TextScale ?? 0;
348 set => TextComponent.TextScale = value;
349 }
350 public Color TextColor
351 {
352 get => TextComponent?.TextColor ?? Color.White;
353 set
354 {
355 if (TextComponent != null)
356 {
357 TextComponent.TextColor = value;
358 }
359 }
360 }
361
362 public event Action<string> OnTextChanged;
363 public Action<string> AddOnTextChanged { set => OnTextChanged += value; }
364 public event Action<string> OnTextAdded;
365 public Action<string> AddOnTextAdded { set => OnTextAdded += value; }
366
367 public override void Draw(SpriteBatch spriteBatch)
368 {
369 if (Focused)
370 {
371 CaretIndicator.Visible = CaretIndicatorVisible && Timing.TotalTime % CaretBlinkInterval > CaretBlinkInterval / 2;
372 }
373
374
375 base.Draw(spriteBatch);
376 }
377
378
379
380 public CUITextInput(string text) : this()
381 {
382 Text = text;
383 }
384 public CUITextInput() : base()
385 {
386 AbsoluteMin = new CUINullRect(w: 50, h: 22);
387 FitContent = new CUIBool2(true, true);
388 Focusable = true;
389 Border.Thickness = 2;
391 ConsumeMouseClicks = true;
392 ConsumeDragAndDrop = true;
393 ConsumeSwipe = true;
394 SerializeChildren = false;
395
396 this["TextComponent"] = TextComponent = new CUITextBlock()
397 {
398 Text = "",
399 Relative = new CUINullRect(0, 0, 1, 1),
400 TextAlign = CUIAnchor.CenterLeft,
401 Style = new CUIStyle(){
402 {"Padding", "[2,2]"},
403 {"TextColor", "CUIPalette.Input.Text"},
404 },
405 };
406
407 this["SelectionOverlay"] = SelectionOverlay = new CUIComponent()
408 {
409 Style = new CUIStyle(){
410 {"BackgroundColor", "CUIPalette.Input.Selection"},
411 {"Border", "Transparent"},
412 },
413 Relative = new CUINullRect(h: 1),
414 Ghost = new CUIBool2(true, true),
416 };
417
418 this["CaretIndicator"] = CaretIndicator = new CUIComponent()
419 {
420 Style = new CUIStyle(){
421 {"BackgroundColor", "CUIPalette.Input.Caret"},
422 {"Border", "Transparent"},
423 },
424 Relative = new CUINullRect(y: 0.1f, h: 0.8f),
425 Absolute = new CUINullRect(w: 1),
426 Ghost = new CUIBool2(true, true),
427 Visible = false,
429 };
430
431 OnConsume += (o) =>
432 {
433 string value = o.ToString();
434 State = new TextInputState(value, State.Selection, State.CaretPos);
435 Valid = IsValidText(value);
436 };
437
438
439
440 OnFocus += () =>
441 {
442 UpdateBorderColor();
443 CaretIndicator.Visible = true;
444 };
445
446 OnFocusLost += () =>
447 {
448 UpdateBorderColor();
449 Selection = IntRange.Zero;
450 CaretIndicator.Visible = false;
451 };
452
453 OnMouseDown += (e) =>
454 {
455 int newCaretPos = SetCaretPos(e.MousePosition - Real.Position);
456 Selection = IntRange.Zero;
457 selectionHandle.lastSelectedPos = newCaretPos;
458 selectionHandle.Grabbed = true;
459 };
460
461 OnMouseMove += (e) =>
462 {
463 if (selectionHandle.Grabbed)
464 {
465 int nextCaretPos = SetCaretPos(e.MousePosition - Real.Position);
466 Selection = new IntRange(selectionHandle.lastSelectedPos, nextCaretPos);
467 }
468 };
469
470 OnDClick += (e) => SelectAll();
471
472 if (CUI.Main is not null)
473 {
474 CUI.Main.Global.OnMouseUp += (e) => selectionHandle.Grabbed = false;
475 }
476 }
477 }
478
479}
Base class for all components.
virtual bool Disabled
Usually means - non interactable, e.g. unclickable gray button.
CUINullRect Relative
Relative to parent size and position, [0..1].
string Command
This command will be dispatched up when some component specific event happens.
CUIStyle Style
Allows you to assing parsable string or link to CUIPalette to any prop It's indexable,...
bool HideChildrenOutsideFrame
Should children be cut off by scissor rect, this is just visual, it's not the same as culling.
CUIBool2 FitContent
Will resize itself to fit components with absolute size, e.g. text.
CUIRect Real
Calculated prop, position on real screen in pixels Should be fully calculated after CUIMainComponent....
bool SerializeChildren
Is this a serialization cutoff point Parent will serialize children down to this component and stop...
bool IgnoreParentVisibility
Don't inherit parent Visibility.
Action< Object > OnConsume
Happens when appropriate data is received.
void DispatchUp(CUICommand command)
Dispathes command up the component tree until someone consumes it.
CUIBool2 Ghost
Ghost components don't affect layout.
bool Visible
Invisible components are not drawn, but still can be interacted with.
CUINullRect Absolute
Absolute size and position in pixels.
In fact a static class managing static things.
Definition CUIErrors.cs:16
static void Log(object msg, Color? color=null, [CallerFilePath] string source="", [CallerLineNumber] int lineNumber=0)
Prints a message to console.
int CaretIndex(float x)
Tndex of caret if there was one Used in CUITextInput, you don't need this.
float CaretPos(int i)
X coordinate of caret if there was one Used in CUITextInput, you don't need this.
void ReceiveSpecialInput(Keys key)
From IKeyboardSubscriber, don't use it directly.
void ReceiveTextInput(char inputChar)
From IKeyboardSubscriber, don't use it directly.
override void Draw(SpriteBatch spriteBatch)
Here component should be drawn.
void ReceiveCommandInput(char command)
From IKeyboardSubscriber, don't use it directly.
void ReceiveEditingInput(string text, int start, int length)
From IKeyboardSubscriber, don't use it directly.
bool Selected
From IKeyboardSubscriber, don't use it directly.
void ReceiveTextInput(string text)
From IKeyboardSubscriber, don't use it directly.
record CUICommand(string Name, object Data=null)
Can be dispatched up the component tree to notify parent about something add pass some event data wit...