Survivalcraft API 1.8.2.3 v1.8.2.3
Survivalcraft 2.4
载入中...
搜索中...
未找到
TextBoxWidget.cs
浏览该文件的文档.
1// ReSharper disable UnusedAutoPropertyAccessor.Global
2
3using Engine;
5using Engine.Input;
6using Engine.Media;
7#if WINDOWS
8using ImeSharp;
9#endif
10
11namespace Game {
22 public class TextBoxWidget : Widget {
23 // 已过时成员
24
25 #region Obsolete Members
26
35 [Obsolete("TextBoxWidget.JustOpened is deprecated.", true)]
36 public bool JustOpened;
37
38 [Obsolete("TextBoxWidget.MoveNextFlag is deprecated.", true)]
39 public bool MoveNextFlag;
40
49#if IOS
50 public string Description { get; set; }
51#else
52
53 public string Description { get => field;
54 set {
55 if (field != value
56 && value != null) {
57 if (value.StartsWith('[')
58 && value.EndsWith(']')) {
59 string[] xp = value.Substring(1, value.Length - 2).Split(':');
60 field = xp.Length == 2 ? LanguageControl.GetContentWidgets(xp[0], xp[1]) : LanguageControl.Get("Usual", value);
61 }
62 else {
63 field = value;
64 }
65 }
66 }
67 }
68
69#endif
70
79#if IOS
80 public string Title { get; set; }
81
82#else
83 public string Title {
84 get => field;
85 set {
86 if (field != value
87 && value != null) {
88 if (value.StartsWith('[')
89 && value.EndsWith(']')) {
90 string[] xp = value.Substring(1, value.Length - 2).Split(':');
91 field = xp.Length == 2 ? LanguageControl.GetContentWidgets(xp[0], xp[1]) : LanguageControl.Get("Usual", value);
92 }
93 else {
94 field = value;
95 }
96 }
97 }
98 }
99#endif
108 [Obsolete($"TextBoxWidget.m_caretPosition is deprecated, use {nameof(TextBoxWidget)}.{nameof(Caret)} instead.", true)]
109 public int m_caretPosition;
110
119 [Obsolete($"TextBoxWidget.CaretPosition is deprecated, use {nameof(TextBoxWidget)}.{nameof(Caret)} instead.", true)]
120 public int CaretPosition {
121 get => Caret;
122 set => Caret = value;
123 }
124
133 [Obsolete($"TextBoxWidget.m_hasFocus is deprecated, use {nameof(TextBoxWidget)}.{nameof(HasFocus)} instead.", true)]
134 public bool m_hasFocus;
135
144 [Obsolete($"TextBoxWidget.m_size is deprecated, use {nameof(TextBoxWidget)}.{nameof(Size)} property instead.", true)]
146
157 [Obsolete($"TextBoxWidget.m_focusStartTime is deprecated, use {nameof(TextBoxWidget)}.{nameof(FocusStartTime)} instead.", true)]
158 public float m_focusStartTime = 0;
159
160#endregion
161
162 // 文本存储
163
164 #region Texts
165
171 public string SelectionString {
172 get {
173 if (SelectionLength == 0) {
174 return null;
175 }
176 int caret = Caret;
177 int selectionLength = SelectionLength;
178 if (SelectionLength < 0) {
179 caret += selectionLength;
180 selectionLength = -selectionLength;
181 }
182 return Text.Substring(caret, selectionLength);
183 }
184 }
185
194 public string FullText => Text.Insert(Caret, CompositionText ?? "");
195
206 public string m_text = "";
207
216 public string Text {
217 get => m_text;
218 set {
219 string text = value == null ? string.Empty :
220 value.Length > MaximumLength ? value.Substring(0, MaximumLength) : value;
221 if (text != m_text) {
222 m_text = text;
223 Caret = Math.Clamp(Caret, 0, m_text.Length);
224 TextChanged?.Invoke(this);
226 }
227 }
228 }
229
230 public void ChangeTextNoEvent(string value) {
231 string text = value == null ? string.Empty :
232 value.Length > MaximumLength ? value.Substring(0, MaximumLength) : value;
233 if (text != m_text) {
234 m_text = text;
235 Caret = Math.Clamp(Caret, 0, m_text.Length);
236 if (!TasksQueue.OfType<SetCursorPositionTask>().Any()) {
237 TasksQueue.Enqueue(new SetCursorPositionTask());
238 }
240 }
241 }
242
257 public string CompositionText { get; set; }
258
277 public int CompositionTextCaret { get; set; }
278
279 public int m_caret;
280
291 public int Caret {
292 get => m_caret;
293 set {
294 if (!TasksQueue.OfType<SetCursorPositionTask>().Any()) {
295 TasksQueue.Enqueue(new SetCursorPositionTask());
296 }
297 m_caret = Math.Clamp(value, 0, Text.Length);
298 }
299 }
300
301 #endregion
302
303 // 交互
304
305 #region Interacting
306
307 public abstract class UpdateTask {
308 public abstract void Run(TextBoxWidget widget);
309 }
310
312 public override void Run(TextBoxWidget widget) {
313 SetCursorPosition(widget);
314 }
315 }
316
320 public Queue<UpdateTask> TasksQueue { get; } = [];
321
330 public int SelectionLength { get; set; }
331
340 public bool SelectionStarted { get; set; }
341
350 public bool ScrollStarted { get; set; }
351
360 public static TextBoxWidget FocusedTextBox { get; set; }
361
372 public double FocusStartTime { get; set; }
373
382 public bool HasFocus {
383 get => FocusedTextBox == this;
384 set {
385 bool originValue = HasFocus;
386 if (value) {
387 FocusedTextBox = this;
388 if (!TasksQueue.OfType<SetCursorPositionTask>().Any()) {
389 TasksQueue.Enqueue(new SetCursorPositionTask());
390 }
391 }
392 else {
393 FocusedTextBox = null;
394 }
395 if (originValue && !value) {
396 FocusLost?.Invoke(this);
397 }
398 if (!originValue && value) {
399 OnFocus?.Invoke(this);
400 }
401 }
402 }
403
415 public static string[] SplitStringAt(string str, int splitPosition) {
416 string left = str[..splitPosition];
417 string right = str[splitPosition..];
418 return [left, right];
419 }
420
453 public void EnterCharacter(char value, int position = -1, bool moveCaret = true) {
454 if (value is '\r') {
455 return;
456 }
457
458 // 换行符的数量 + 1 即为行数 所以在此处加上 1
459 if (value is '\n'
460 && Text.Sum(x => x == '\n' ? 1 : 0) + 1 >= MaximumLinesCount) {
461 value = ' ';
462 }
463 if (SelectionLength != 0) {
465 }
467 if (OverwriteMode) {
468 OverwriteCharacter();
469 return;
470 }
471 InsertCharacter();
472 return;
473
474 void OverwriteCharacter() {
475 if (position is -1) {
476 position = Caret;
477 }
478 string str;
479 if (value is '\t' && IndentAsSpace) {
480 int distanceToNextIndent = IndentWidth - position % IndentWidth;
481 str = new string(' ', distanceToNextIndent);
482 }
483 else {
484 str = value.ToString();
485 }
486 if (str.Length + Text.Length > MaximumLength) {
487 str = str[..(MaximumLength - Text.Length)];
488 }
489 Text = Text.Remove(position, str.Length);
490 Text = Text.Insert(position, str);
491 }
492
493 void InsertCharacter() {
494 if (Text.Length >= MaximumLength) {
495 return;
496 }
497 if (position is -1) {
498 position = Caret;
499 }
500 string str;
501 if (value is '\t' && IndentAsSpace) {
502 int distanceToNextIndent = IndentWidth - position % IndentWidth;
503 str = new string(' ', distanceToNextIndent);
504 }
505 else {
506 str = value.ToString();
507 }
508 if (str.Length + Text.Length > MaximumLength) {
509 str = str[..(MaximumLength - Text.Length)];
510 }
511 Text = Text.Insert(position, str);
512 if (moveCaret) {
513 Caret += str.Length;
514 }
515 }
516 }
517
526 public void DeleteSelection() {
527 int caret = Caret;
528 int selectionLength = SelectionLength;
529 if (SelectionLength < 0) {
530 caret += selectionLength;
531 selectionLength = -selectionLength;
532 }
533 Text = Text.Remove(caret, selectionLength);
534 SelectionLength = 0;
535 Caret = Math.Clamp(caret, 0, Text.Length);
536 }
537
543 public void EnterText(string value) {
544 EnterText(value, Caret);
545 }
546
555 public void EnterText(string value, int index) {
556 foreach (char character in value.ReplaceLineEndings("\n")) {
557 EnterCharacter(character, index++);
558 }
559 }
560
571 public static string[] CharacterKindsMap = ["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789"];
572
605 public void BackSpace(char? character = null, int count = 1, bool moveCaret = true) {
606 if (SelectionLength != 0) {
608 return;
609 }
610 string map = character.HasValue ? CharacterKindsMap.FirstOrDefault(x => x.Contains(character.Value), character.ToString()) : null;
611 int i;
612 for (i = Caret; i > 0 && count != 0; i--) {
613 if (map != null
614 && !map.Contains(Text[i - 1])) {
615 break;
616 }
617 Text = Text.Remove(i - 1, 1);
618 count--;
619 }
620 if (moveCaret) {
621 Caret = i;
622 }
624 }
625
650 public void Delete(char? character = null, int count = 1) {
651 if (SelectionLength != 0) {
653 return;
654 }
655 string map = character.HasValue ? CharacterKindsMap.FirstOrDefault(x => x.Contains(character.Value), character.ToString()) : null;
656 for (; count != 0 && Caret < Text.Length; count--) {
657 if (map != null
658 && !map.Contains(Text[Caret])) {
659 break;
660 }
661 Text = Text.Remove(Caret, 1);
662 }
664 }
665
666 static TextBoxWidget() {
667#if WINDOWS
668 InputMethod.TextCompositionCallback += (text, pos) => {
669 if (FocusedTextBox is null) {
670 return;
671 }
672 if (FocusedTextBox.SelectionLength != 0) {
673 FocusedTextBox.DeleteSelection();
674 }
675 FocusedTextBox.CompositionText = text;
676 FocusedTextBox.CompositionTextCaret = pos;
677 FocusedTextBox.FocusStartTime = Time.RealTime;
678 };
679 InputMethod.TextInputCallback += character => {
680 if (FocusedTextBox is null) {
681 return;
682 }
683 FocusedTextBox.CompositionText = null;
684 switch (character) {
685 case '\b': {
686 FocusedTextBox.BackSpace();
687 break;
688 }
689 case (char)127: {
690 if (FocusedTextBox.Caret is 0) {
691 break;
692 }
693 FocusedTextBox.BackSpace(FocusedTextBox.Text[FocusedTextBox.Caret - 1], -1);
694 break;
695 }
696 case (char)3: {
697 // Ctrl + C
698 if (FocusedTextBox == null) {
699 break;
700 }
701 if (Keyboard.IsKeyDown(Key.Control)
702 && Keyboard.IsKeyDownOnce(Key.C)) {
704 }
705 break;
706 }
707 case (char)1: {
708 // Ctrl + V
709 if (FocusedTextBox == null) {
710 break;
711 }
712 FocusedTextBox.Caret = 0;
713 FocusedTextBox.SelectionLength = FocusedTextBox.Text.Length;
714 break;
715 }
716 case (char)22: {
717 // Ctrl + V
718 if (FocusedTextBox == null) {
719 break;
720 }
721 string text = ClipboardManager.ClipboardString;
722 if (text != null) {
723 FocusedTextBox.EnterText(text);
724 }
725 break;
726 }
727 case (char)24: {
728 // Ctrl + X
729 if (FocusedTextBox == null) {
730 break;
731 }
733 FocusedTextBox.DeleteSelection();
734 break;
735 }
736 case (char)27: {
737 // Escape
738 break;
739 // TextBoxWidget 的 Esc 处理不依赖此输入,所以直接跳过。
740 }
741 case (< (char)32 or > (char)126) and <= (char)128: { // ASCII 码表内 0-128 的字符,如果不在 32-126 范围内,则跳过。
742 break;
743 }
744 default: {
745 FocusedTextBox.EnterCharacter(character);
746 break;
747 }
748 }
749 };
750/*#elif ANDROID
751 Window.Activity.OnDispatchKeyEvent += keyEvent =>
752 {
753 if (FocusedTextBox == null)
754 {
755 return false;
756 }
757
758 if (keyEvent.Characters == "\t")
759 {
760
761 FocusedTextBox.Enter?.Invoke(FocusedTextBox);
762 return true;
763 }
764 FocusedTextBox.EnterText(keyEvent.Characters ?? "");
765 return keyEvent.Characters != null && keyEvent.Characters.Length > 0;
766 };*/
767#endif
768 }
769
778 public double DragStartTime { get; set; } = -1;
779
785 public Vector2? LastDragPosition { get; set; }
786
795 public bool DragStartedInsideTextBox { get; set; }
796
797 public override void UpdateCeases() {
798 if (HasFocus) {
800 }
801 }
802
803 public override void Update() {
804 foreach (UpdateTask task in TasksQueue) {
805 task.Run(this);
806 }
807 TasksQueue.Clear();
808 if (Input.Click.HasValue) {
809 // 处理点击,使文本框在被点击时获取焦点。
810 if (HitTestGlobal(Input.Click.Value.Start) == this
811 && HitTestGlobal(Input.Click.Value.End) == this) {
813#if WINDOWS
815#elif ANDROID || BROWSER
817 Title ?? "",
818 Description ?? "",
819 Text,
821 text => {
822 Text = text;
823 HasFocus = false;
824 },
825 () => HasFocus = false
826 );
827#endif
828 HasFocus = true;
829 Caret = CalculateClickedCharacterIndex(
830 Font,
831 PasswordMode ? new string('*', Text.Length) : Text,
832 ScreenToWidget(Input.Click.Value.Start) + new Vector2(Scroll, 0),
833 FontScale,
836 );
837 }
838 else if (FocusedTextBox == this) {
839 // 如果点击不在文本框内,则失去焦点。
841 HasFocus = false;
842 }
843 SelectionLength = 0;
844 }
845#if !ANDROID
846 if (Input.Scroll.HasValue
847 && Input.MousePosition.HasValue
848 && HitTestGlobal(Input.MousePosition.Value) == this) {
849 float scroll = Input.Scroll.Value.X * Input.Scroll.Value.Z / 92;
850 Scroll -= scroll;
851 }
852 if (Input.Drag.HasValue) {
853 if (DragStartTime < 0) {
854 // 拖拽刚开始时:
856 SelectionLength = 0;
857 if (HitTestGlobal(Input.Drag.Value) == this) {
858 HasFocus = true;
859 SelectionStarted = true;
861 Caret = CalculateClickedCharacterIndex(
862 Font,
863 PasswordMode ? new string('*', Text.Length) : Text,
864 ScreenToWidget(Input.Drag.Value) + new Vector2(Scroll, 0),
865 FontScale,
868 );
870 }
871 else {
872 SelectionStarted = false;
874 HasFocus = false;
875 }
876 }
878 // 拖拽正在进行时:
879 int caret2 = CalculateClickedCharacterIndex(
880 Font,
881 PasswordMode ? new string('*', Text.Length) : Text,
882 ScreenToWidget(Input.Drag.Value) + new Vector2(Scroll, 0),
883 FontScale,
886 );
887 if (SelectionStarted) {
888 SelectionLength = caret2 - Caret;
889 }
890 if (Math.Abs(caret2 - Caret) > 1) {
891 ScrollStarted = true;
892 }
893 }
894 LastDragPosition = Input.Drag.Value;
895 }
896 else if (DragStartTime >= 0
897 && !Input.Drag.HasValue) {
898 DragStartTime = -1;
899 SelectionStarted = false;
900 ScrollStarted = false;
901 }
902
903 // 处理光标移动。
904 if (HasFocus && string.IsNullOrEmpty(CompositionText)) {
905 if (Keyboard.IsKeyDownRepeat(Key.LeftArrow)) {
906 Caret = Math.Max(0, Caret - 1);
907 SelectionLength = 0;
908 SelectionStarted = false;
910 }
911 if (Keyboard.IsKeyDownRepeat(Key.RightArrow)) {
912 Caret = Math.Min(Text.Length, Caret + 1);
913 SelectionLength = 0;
914 SelectionStarted = false;
916 }
917 if (Keyboard.IsKeyDownOnce(Key.Home)
918 || Keyboard.IsKeyDownOnce(Key.UpArrow)) {
919 Caret = 0;
920 SelectionLength = 0;
921 SelectionStarted = false;
923 }
924 if (Keyboard.IsKeyDownOnce(Key.End)
925 || Keyboard.IsKeyDownOnce(Key.DownArrow)) {
926 Caret = Text.Length;
927 SelectionLength = 0;
928 SelectionStarted = false;
930 }
931 }
932 if (HasFocus && Keyboard.IsKeyDownOnce(Key.Escape)) {
933 Escape?.Invoke(this);
934 }
935
936//#if !ANDROID
937 // 处理 Delete 键。
938 if (HasFocus
939 && Caret != Text.Length
940 && Keyboard.IsKeyDownRepeat(Key.Delete)) {
941 if (Keyboard.IsKeyDown(Key.Control)) {
942 Delete(count: -1, character: Text[Caret]);
943 }
944 else {
945 Delete();
946 }
947 }
948 if (HasFocus && Keyboard.IsKeyDownOnce(Key.Enter)) {
949 if (EnterAsNewLine) {
950 EnterCharacter('\n');
951 }
952 else {
953 Enter?.Invoke(this);
954 HasFocus = false;
956 }
957 }
958//#endif
959
960 // 处理键盘输入。
961 // 如果输入法已开启,就跳过。
963 // 处理 BackSpace 键。
964 if (Caret != 0
965 && (Keyboard.IsKeyDownRepeat(Key.Delete) || Keyboard.IsKeyDownRepeat(Key.BackSpace))) {
966 if (Keyboard.IsKeyDown(Key.Control)) {
967 BackSpace(count: -1, character: Text[Caret - 1]);
968 }
969 else {
970 BackSpace();
971 }
973 }
974
975 // 如果 Tab 键切换文本框的功能未启用,则将 Tab 键视为制表符输入。
977 && Keyboard.IsKeyDownRepeat(Key.Tab)) {
978 EnterCharacter('\t');
979 }
980
981 // 处理文本输入。
982 char? lastChar = Keyboard.LastChar;
983 if (lastChar != null
984 && lastChar != '\n') {
985 EnterCharacter(lastChar.Value);
986 }
987 }
988
989 // 处理 Tab 键切换文本框。
990 if (HasFocus
992 && Keyboard.IsKeyDownRepeat(Key.Tab)) {
993 if (RootWidget is not ContainerWidget rootWidget) {
994 return;
995 }
996 List<TextBoxWidget> textBoxes = FindTextBoxWidgets(rootWidget);
997 int thisIndex = textBoxes.IndexOf(this);
998 FocusedTextBox = textBoxes[(thisIndex + 1) % textBoxes.Count];
999 }
1000 if (HasFocus
1001 && SelectionLength != 0
1002 && !InputMethodEnabled) {
1003 if (Keyboard.IsKeyDown(Key.Control)
1004 && Keyboard.IsKeyDownOnce(Key.C)) {
1006 }
1007 if (Keyboard.IsKeyDown(Key.Control)
1008 && Keyboard.IsKeyDownOnce(Key.X)) {
1011 }
1012 if (Keyboard.IsKeyDown(Key.Control)
1013 && Keyboard.IsKeyDownOnce(Key.V)) {
1014 string text = ClipboardManager.ClipboardString;
1015 if (text != null) {
1017 }
1018 }
1019 }
1020 return;
1021#endif
1022
1023 // ReSharper disable UnusedLocalFunction
1024 static List<TextBoxWidget> FindTextBoxWidgets(ContainerWidget widget)
1025 // ReSharper restore UnusedLocalFunction
1026 {
1027 List<TextBoxWidget> textBoxes = new(16);
1028 foreach (Widget child in widget.Children) {
1029 if (child is TextBoxWidget textBoxWidget) {
1030 textBoxes.Add(textBoxWidget);
1031 }
1032 if (child is not ContainerWidget containerWidget) {
1033 continue;
1034 }
1035 List<TextBoxWidget> result = FindTextBoxWidgets(containerWidget);
1036 if (result.Count is not 0) {
1037 textBoxes.AddRange(result);
1038 }
1039 }
1040 return textBoxes;
1041 }
1042
1043 static int CalculateClickedCharacterIndex(BitmapFont font,
1044 string text,
1045 Vector2 clickPosition,
1046 float fontScale,
1047 Vector2 fontSpacing,
1048 Vector2 widgetActualSize) {
1049 float scale = fontScale * font.Scale;
1050 Vector2 spacing = fontSpacing + font.Spacing;
1051 float currentPosition = 0f;
1052 float currentHeight = widgetActualSize.Y / 2f - font.LineHeight * scale / 2;
1053 int i = 0;
1054 while (currentHeight + font.LineHeight * scale + spacing.Y < clickPosition.Y) {
1055 i = text.IndexOf('\n', i) + 1;
1056 if (i == -1) {
1057 return text.Length;
1058 }
1059 currentHeight += font.LineHeight * scale + spacing.Y;
1060 }
1061 for (; i < text.Length; i++) {
1062 char letter = text[i];
1063 if (letter == '\n') {
1064 return i;
1065 }
1066 if (letter == '\u200b') {
1067 continue;
1068 }
1069 BitmapFont.Glyph glyph = font.GetGlyph(letter is '\u00a0' ? ' ' : letter);
1070 float kerning = i + 1 < text.Length ? font.GetKerning(letter, text[i + 1]) : 0f;
1071 if (currentPosition + (glyph.Width - kerning + spacing.X) / 2 > clickPosition.X) {
1072 return i;
1073 }
1074 currentPosition += (glyph.Width - kerning + spacing.X) * scale;
1075 }
1076 return text.Length;
1077 }
1078 }
1079
1085 public static void ShowInputMethod() {
1086#if WINDOWS
1087 InputMethodEnabled = true;
1088#endif
1089 }
1090
1091 public static void CloseInputMethod() {
1092#if WINDOWS
1093 InputMethodEnabled = false;
1094#endif
1095 }
1096
1097 #endregion
1098
1099 // 选项
1100
1101 #region Options
1102
1111 public static bool ShowCandidatesWindow { get; set; } = true;
1112
1130 public bool IndentAsSpace { get; set; } = false;
1131
1140 public bool EnterAsNewLine { get; set; } = false;
1141
1156 public int IndentWidth { get; set; } = 4;
1157
1168 public int m_maximumLength = 512;
1169
1179 public int MaximumLength {
1180 get => m_maximumLength;
1181 set {
1182 if (value < 0) {
1183 throw new InvalidOperationException($"{nameof(MaximumLength)} 必须大于或等于 0.");
1184 }
1185 if (m_maximumLength > value) {
1186 if (Text.Length > value) {
1187 Text = Text[..value];
1188 }
1189 Caret = Math.Clamp(Caret, 0, value);
1190 }
1191 m_maximumLength = value;
1192 }
1193 }
1194
1195 public int m_maximumLinesCount = 1;
1196
1198 get => m_maximumLinesCount;
1199 set {
1200 if (value < 0) {
1201 throw new InvalidOperationException($"{nameof(MaximumLength)} 必须大于或等于 0.");
1202 }
1203 if (m_maximumLinesCount > value) {
1204 if (Text.Sum(x => x == '\n' ? 1 : 0) > value) {
1205 Text = Text[..Text.IndexOf('\n', 0, value)];
1206 }
1207 Caret = Math.Clamp(Caret, 0, value);
1208 }
1209 m_maximumLinesCount = value;
1210 }
1211 }
1212
1221 public bool PasswordMode { get; set; }
1222
1231 public bool OverwriteMode { get; set; }
1232
1248 public bool SwitchTextBoxWhenTabbed { get; set; } = true;
1249
1250 #endregion
1251
1252 // 外观
1253
1254 #region Appearance
1255
1256 public Color Color { get; set; } = Color.White;
1257
1258 public float m_scroll;
1259
1260 public float Scroll {
1261 get => m_scroll;
1262 set {
1263 m_scroll = value;
1265 }
1266 }
1267
1271 public void LimitScrollValue() {
1272 const double tolerance = 0.01;
1273 float scrollRange = Font.MeasureText(FullText, new Vector2(FontScale), FontSpacing).X - Size.X;
1274 if (scrollRange < 0) {
1275 scrollRange = 0;
1276 }
1277 float newValue = Math.Clamp(m_scroll, 0, scrollRange);
1278 if (Math.Abs(newValue - m_scroll) > tolerance) {
1279 m_scroll = newValue;
1280 // 请勿改为 Scroll 属性,会引起无限递归
1281 if (!TasksQueue.OfType<SetCursorPositionTask>().Any()) {
1282 TasksQueue.Enqueue(new SetCursorPositionTask());
1283 }
1284 }
1285 }
1286
1295 public Color OutlineColor { get; set; } = Color.White;
1296
1305 public Color CandidateSelectionColor { get; set; } = Color.Red;
1306
1315 public Color CandidateTextColor { get; set; } = Color.White;
1316
1325 public bool AutoSize { get; set; } = true;
1326
1336
1345 public Vector2 Size {
1346 get => m_sizeValue;
1347 set {
1348 m_sizeValue = value;
1349 AutoSize = false;
1350 }
1351 }
1352
1361 public Vector2 FontSpacing { get; set; }
1362
1371 public float FontScale { get; set; } = 1;
1372
1381 public bool TextureLinearFilter { get; set; } = true;
1382
1393 public Vector2 CandidateListOffset { get; set; } = new(0, 16);
1394
1403 public float CandidatesSpacing { get; set; } = 4;
1404
1413 public BitmapFont Font { get; set; } = ContentManager.Get<BitmapFont>("Fonts/Pericles");
1414
1423 public float CandidateWindowLength { get; set; } = 128;
1424
1434
1435 #endregion
1436
1437 // 输入法 Api
1438
1439 #region Input Method Api
1440
1441 public TextBoxWidget() {
1442 TextChanged += _ => {
1443 if (!TasksQueue.OfType<SetCursorPositionTask>().Any()) {
1444 TasksQueue.Enqueue(new SetCursorPositionTask());
1445 }
1446 };
1447 }
1448
1449 // ReSharper disable UnusedParameter.Local
1451 // ReSharper restore UnusedParameter.Local
1452 {
1453#if WINDOWS
1454 Vector2 caretPosition = widget.Font.MeasureText(
1455 widget.FullText,
1456 0,
1457 widget.Caret + widget.CompositionTextCaret,
1458 new Vector2(widget.FontScale),
1459 widget.FontSpacing
1460 );
1461 Vector2 windowPosition = Vector2.Transform(
1462 new Vector2(caretPosition.X, 0),
1463 Matrix.CreateTranslation(-widget.Scroll, 0, 0) * widget.GlobalTransform
1464 );
1465 InputMethod.SetTextInputRect((int)windowPosition.X, (int)(windowPosition.Y + widget.Font.LineHeight * widget.GlobalTransform.M11), 0, 0);
1466#endif
1467 }
1468
1477 public static bool InputMethodEnabled {
1478#if WINDOWS
1479 get => InputMethod.Enabled;
1480 set => InputMethod.Enabled = value;
1481#else
1482 get => false;
1483 // ReSharper disable ValueParameterNotUsed
1484 set { }
1485 // ReSharper restore ValueParameterNotUsed
1486#endif
1487 }
1488
1499 public static string[] CandidatesList =>
1500#if WINDOWS
1501 InputMethod.CandidateList.Select(x => x.ToString()).ToArray();
1502#else
1503 [];
1504#endif
1505
1514 public static int CandidatesSelection =>
1515#if WINDOWS
1516 InputMethod.CandidateSelection;
1517#else
1518 -1;
1519#endif
1520
1529 public static int CandidatesPageSize =>
1530#if WINDOWS
1531 InputMethod.CandidatePageSize;
1532#else
1533 0;
1534#endif
1535
1536 #endregion
1537
1538 // 事件
1539
1540 #region Events
1541
1550 public event Action<TextBoxWidget> FocusLost;
1551
1560 public event Action<TextBoxWidget> OnFocus;
1561
1570#pragma warning disable CS0067 // 事件从未使用过
1571 public event Action<TextBoxWidget> Enter;
1572
1581 public event Action<TextBoxWidget> Escape;
1582#pragma warning restore CS0067 // 事件从未使用过
1583
1592 public event Action<TextBoxWidget> TextChanged;
1593
1594 #endregion
1595
1596 // 绘制
1597
1598 #region Render
1599
1600 public override void Overdraw(DrawContext dc) {
1601 if (CandidatesPageSize is 0
1603 || !HasFocus) {
1604 return;
1605 }
1607 Display.ScissorRectangle = Display.Viewport.Rectangle;
1608 FlatBatch2D backgroundFlatBatch = dc.PrimitivesRenderer2D.FlatBatch();
1609 FlatBatch2D outlineFlatBatch = dc.PrimitivesRenderer2D.FlatBatch(1);
1610 FlatBatch2D foregroundFlatBatch = dc.PrimitivesRenderer2D.FlatBatch(2);
1612 Font,
1613 3,
1615 );
1616 Vector2 caretPosition = Font.MeasureText(FullText, 0, Caret + CompositionTextCaret, new Vector2(FontScale), FontSpacing) * Vector2.UnitX;
1617 Vector2 candidateWindowCorner1 = new(caretPosition.X, ActualSize.Y);
1618 candidateWindowCorner1 += CandidateListOffset;
1619
1620 // 绘制背景。
1621 backgroundFlatBatch.QueueQuad(
1622 Vector2.Zero,
1624 0,
1626 );
1627 outlineFlatBatch.QueueRectangle(
1628 Vector2.Zero,
1630 0,
1632 );
1633
1634 // 绘制候选词文字。
1636 // 获取候选词文字,并在前面加上序号。
1637 string candidate = $"{i + 1} {CandidatesList[i]}";
1638
1639 // 遍历计算当前文本长度
1640 float width = 0;
1641 int characterIndex = 0;
1642 while (characterIndex < candidate.Length) {
1643 BitmapFont.Glyph glyph;
1644 try {
1645 glyph = Font.GetGlyph(candidate[characterIndex]);
1646 }
1647 catch {
1648 glyph = Font.FallbackGlyph;
1649 }
1650 width += glyph.Width * FontScale * Font.Scale;
1651 if (width >= CandidateWindowLength - 8 - 2 * Font.GetGlyph('.').Width) {
1652 break;
1653 }
1654 characterIndex++;
1655 }
1656
1657 // 如果文本长度超出窗口长度,则截断并替换位 “.."。
1658 if (width >= CandidateWindowLength - 8 - 2 * Font.GetGlyph('.').Width) {
1659 candidate = candidate[..characterIndex];
1660 candidate += "..";
1661 }
1662
1663 // 绘制文本。
1664 fontBatch.QueueText(
1665 candidate,
1666 new Vector2(2, i * Font.GlyphHeight + i * CandidatesSpacing),
1667 0,
1669 TextAnchor.Left | TextAnchor.Top,
1670 new Vector2(FontScale),
1672 );
1673 }
1674 foregroundFlatBatch.TransformTriangles(Matrix.CreateTranslation(new Vector3(candidateWindowCorner1, 0)));
1675 foregroundFlatBatch.TransformTriangles(GlobalTransform);
1676 backgroundFlatBatch.TransformTriangles(Matrix.CreateTranslation(new Vector3(candidateWindowCorner1, 0)));
1677 backgroundFlatBatch.TransformTriangles(GlobalTransform);
1678 outlineFlatBatch.TransformLines(Matrix.CreateTranslation(new Vector3(candidateWindowCorner1, 0)));
1679 outlineFlatBatch.TransformLines(GlobalTransform);
1680 fontBatch.TransformTriangles(Matrix.CreateTranslation(new Vector3(candidateWindowCorner1, 0)));
1682 ClampToBounds = true;
1684 Display.ScissorRectangle = rect;
1685 }
1686
1687 public override void Draw(DrawContext dc) {
1688 try {
1689 Draw_(dc);
1690 }
1691 catch (Exception e) {
1692 Log.Error(e);
1693 }
1694 }
1695
1696 public virtual void Draw_(DrawContext dc) {
1697 string textToDraw = Text.Replace("\t", new string(' ', IndentWidth));
1698 int caretIndex = Text[..Caret].Sum(c => c == '\t' ? IndentWidth : 1);
1699 int selectionLength = SelectionLength == 0
1700 ? 0
1701 : SelectionString.Sum(c => c == '\t' ? IndentWidth : 1) * (SelectionLength / Math.Abs(SelectionLength));
1702 // 保持 selectionLength 正负与 SelectionLength 一致
1703 int selectionStart = caretIndex;
1704 if (SelectionLength < 0) {
1705 selectionStart += selectionLength;
1706 selectionLength = -selectionLength;
1707 }
1708 if (PasswordMode) {
1709 textToDraw = new string('*', textToDraw.Length);
1710 }
1712 FlatBatch2D outlineFlatBatch = dc.PrimitivesRenderer2D.FlatBatch(1);
1713 Vector2 currentDrawPosition = (0, ActualSize.Y / 2);
1714 List<TextDrawItem> drawItems = new(3);
1716 FlatBatch2D underlineFlatBatch = dc.PrimitivesRenderer2D.FlatBatch(1);
1717 string[] lines = textToDraw.Split('\n');
1718 int charIndex = 0;
1719 foreach (string line in lines) {
1720 if (selectionLength != 0
1721 && selectionStart < charIndex + line.Length
1722 && selectionStart + selectionLength > charIndex) { //如果这一行有字符被选中
1723 int lineStart = charIndex;
1724 int selectionStartInLine = Math.Max(selectionStart - lineStart, 0);
1725 int selectionEndInLine = Math.Min(selectionStart + selectionLength - lineStart, line.Length);
1726 int actualSelectionLength = selectionEndInLine - selectionStartInLine;
1727 drawItems.Add(
1729 flatBatch,
1730 line,
1731 selectionStartInLine,
1732 actualSelectionLength,
1733 Font,
1735 (64, 64, 255, 128),
1736 Font.GlyphHeight * FontScale * Font.Scale,
1737 new Vector2(FontScale)
1738 )
1739 );
1740 }
1741 if (charIndex <= caretIndex
1742 && charIndex + line.Length >= caretIndex) {
1743 string[] split = SplitStringAt(line, caretIndex - charIndex);
1744 drawItems.Add(
1745 new NormalDrawItem(
1746 line,
1747 0,
1748 split[0].Length,
1749 fontBatch,
1750 FontScale,
1752 Color
1753 )
1754 );
1755 if (SelectionLength == 0
1756 && FocusedTextBox == this
1757 && ((Time.RealTime - FocusStartTime - 0.4) % 1.0 <= 0.3f || (Time.RealTime - FocusStartTime - 0.4) % 1.0 >= 0.8f)) {
1758 drawItems.Add(
1759 new CaretDrawItem(
1760 flatBatch,
1761 1,
1762 Font.GlyphHeight * FontScale * Font.Scale,
1765 Font,
1767 Color.White,
1768 new Vector2(FontScale)
1769 )
1770 );
1771 }
1772 drawItems.Add(new CompositionTextDrawItem(CompositionText ?? "", fontBatch, underlineFlatBatch, FontScale, FontSpacing, Color));
1773 if (split.Length > 1) {
1774 drawItems.Add(
1775 new NormalDrawItem(
1776 line,
1777 split[0].Length,
1778 split[1].Length,
1779 fontBatch,
1780 FontScale,
1782 Color
1783 )
1784 );
1785 }
1786 }
1787 else {
1788 drawItems.Add(
1789 new NormalDrawItem(
1790 textToDraw,
1791 charIndex,
1792 line.Length,
1793 fontBatch,
1794 FontScale,
1796 Color
1797 )
1798 );
1799 }
1800 drawItems.Add(new EndOfLineDrawItem(Font, FontSpacing, FontScale));
1801 charIndex += line.Length + 1; // + 1 是因为换行符
1802 }
1803 foreach (TextDrawItem drawItem in drawItems) {
1804 drawItem.Draw(ref currentDrawPosition);
1805 }
1806 Matrix scrollTransform = Matrix.CreateTranslation(new Vector3(-Scroll, 0, 0));
1807 flatBatch.TransformTriangles(scrollTransform);
1808 fontBatch.TransformTriangles(scrollTransform);
1809 underlineFlatBatch.TransformLines(scrollTransform);
1810 fontBatch.TransformTriangles(GlobalTransform);
1811 flatBatch.TransformTriangles(GlobalTransform);
1812 flatBatch.TransformLines(GlobalTransform);
1813 outlineFlatBatch.TransformLines(GlobalTransform);
1814 }
1815
1817 public override void MeasureOverride(Vector2 parentAvailableSize) {
1818 DesiredSize = Size;
1819 IsDrawRequired = true;
1820 IsOverdrawRequired = true;
1821 ClampToBounds = true;
1822 if (!AutoSize) {
1823 return;
1824 }
1825 DesiredSize = Font.MeasureText(Text.Length == 0 ? " " : Text, new Vector2(FontScale), FontSpacing);
1826 base.MeasureOverride(parentAvailableSize);
1827 }
1828
1829 public abstract class TextDrawItem {
1830 public abstract void Draw(ref Vector2 position);
1831 }
1832
1833 public class NormalDrawItem(string fullText,
1834 int start,
1835 int length,
1836 FontBatch2D fontBatch,
1837 float fontScale,
1838 Vector2 fontSpacing,
1839 Color color) : TextDrawItem {
1840 public override void Draw(ref Vector2 position) {
1841 if (length == 0) {
1842 return;
1843 }
1844 BitmapFont font = fontBatch.Font;
1845 Vector2 size = font.MeasureText(fullText, start, length, new Vector2(fontScale), fontSpacing);
1846 fontBatch.QueueText(
1847 fullText.Substring(start, length),
1848 position,
1849 0,
1850 color,
1851 TextAnchor.VerticalCenter,
1852 new Vector2(fontScale),
1853 fontSpacing
1854 );
1855 position.X += size.X;
1856 }
1857 }
1858
1859 public class CompositionTextDrawItem(string compositionText,
1860 FontBatch2D fontBatch,
1861 FlatBatch2D underlineFlatBatch,
1862 float fontScale,
1863 Vector2 fontSpacing,
1864 Color color) : TextDrawItem {
1865 public override void Draw(ref Vector2 position) {
1866 BitmapFont font = fontBatch.Font;
1867 Vector2 size = font.MeasureText(compositionText, 0, compositionText.Length, new Vector2(fontScale), Vector2.Zero);
1868 fontBatch.QueueText(
1869 compositionText,
1870 position,
1871 0,
1872 color,
1873 TextAnchor.VerticalCenter,
1874 new Vector2(fontScale),
1875 fontSpacing
1876 );
1877 underlineFlatBatch.QueueLine(
1878 position + size / 2 * Vector2.UnitY,
1879 position + size / 2 * Vector2.UnitY + new Vector2(size.X, 0),
1880 0,
1881 Color.White
1882 );
1883 position.X += size.X;
1884 }
1885 }
1886
1887 public class CaretDrawItem(FlatBatch2D flatBatch,
1888 float width,
1889 float height,
1890 string compositionText,
1891 int compositionTextCaret,
1892 BitmapFont font,
1893 Vector2 fontSpacing,
1894 Color color,
1895 Vector2 fontScale) : TextDrawItem {
1896 public override void Draw(ref Vector2 position) {
1897 Vector2 offset = font.MeasureText(compositionText, 0, compositionTextCaret, fontScale, fontSpacing) * Vector2.UnitX;
1898 flatBatch.QueueQuad(position + offset + (0, -height / 2), position + offset + (width, height / 2), 0, color);
1899 }
1900 }
1901
1902 public class SelectionDrawItem(FlatBatch2D flatBatch,
1903 string text,
1904 int relativeCaretPosition,
1905 int selectionLength,
1906 BitmapFont font,
1907 Vector2 fontSpacing,
1908 Color color,
1909 float height,
1910 Vector2 fontScale) : TextDrawItem {
1911 int m_relativeCaretPosition = relativeCaretPosition;
1912 int m_selectionLength = selectionLength;
1913
1914 public override void Draw(ref Vector2 position) {
1915 if (m_relativeCaretPosition < 0) {
1918 }
1919 if (m_relativeCaretPosition + m_selectionLength + 1 > text.Length) {
1921 }
1922 float length = font.MeasureText(text, m_relativeCaretPosition, m_selectionLength, fontScale, fontSpacing).X;
1923 float offset = font.MeasureText(text, 0, m_relativeCaretPosition, fontScale, fontSpacing).X;
1924 flatBatch.QueueQuad(position + (offset, -height / 2), position + (offset + length, height / 2), 0, color);
1925 }
1926 }
1927
1928 public class EndOfLineDrawItem(BitmapFont font, Vector2 fontSpacing, float fontScale) : TextDrawItem {
1929 public override void Draw(ref Vector2 position) {
1930 position.X = 0;
1931 position.Y += font.GlyphHeight * font.Scale * fontScale + fontSpacing.Y;
1932 }
1933 }
1934
1935 #endregion
1936 }
1937}
Engine.Vector3 Vector3
void TransformLines(Matrix matrix, int start=0, int end=-1)
void TransformTriangles(Matrix matrix, int start=0, int end=-1)
void TransformTriangles(Matrix matrix, int start=0, int end=-1)
static readonly BlendState NonPremultiplied
static Rectangle ScissorRectangle
static Viewport Viewport
void QueueQuad(Vector2 corner1, Vector2 corner2, float depth, Color color)
void QueueRectangle(Vector2 corner1, Vector2 corner2, float depth, Color color)
void QueueText(string text, Vector2 position, float depth, Color color, TextAnchor anchor=TextAnchor.Default)
FlatBatch2D FlatBatch(int layer=0, DepthStencilState depthStencilState=null, RasterizerState rasterizerState=null, BlendState blendState=null)
void Flush(bool clearAfterFlush=true, int maxLayer=int.MaxValue)
FontBatch2D FontBatch(BitmapFont font=null, int layer=0, DepthStencilState depthStencilState=null, RasterizerState rasterizerState=null, BlendState blendState=null, SamplerState samplerState=null)
static bool IsKeyDown(Key key)
static void ShowKeyboard(string title, string description, string defaultText, bool passwordMode, Action< string > enter, Action cancel)
static bool IsKeyDownRepeat(Key key)
static bool IsKeyDownOnce(Key key)
static void Error(object message)
定义 Log.cs:80
float GetKerning(char code, char followingCode)
Vector2 MeasureText(string text, Vector2 scale, Vector2 spacing)
static double RealTime
定义 Time.cs:38
readonly WidgetsList Children
static object Get(Type type, string name)
static string GetContentWidgets(string name, string prop)
static string Get(string className, int key)
获取在当前语言类名键对应的字符串
override void Draw(ref Vector2 position)
override void Draw(ref Vector2 position)
override void Draw(ref Vector2 position)
override void Draw(ref Vector2 position)
override void Draw(ref Vector2 position)
override void Run(TextBoxWidget widget)
void Draw(ref Vector2 position)
void Run(TextBoxWidget widget)
Action< TextBoxWidget > TextChanged
Action< TextBoxWidget > Enter
static string[] SplitStringAt(string str, int splitPosition)
void LimitScrollValue()
限制 Scroll 属性的值。
virtual void Draw_(DrawContext dc)
override void UpdateCeases()
Action< TextBoxWidget > Escape
static TextBoxWidget FocusedTextBox
Action< TextBoxWidget > FocusLost
void BackSpace(char? character=null, int count=1, bool moveCaret=true)
static string[] CharacterKindsMap
static string[] CandidatesList
Action< TextBoxWidget > OnFocus
override void MeasureOverride(Vector2 parentAvailableSize)
void EnterText(string value)
void EnterCharacter(char value, int position=-1, bool moveCaret=true)
Queue< UpdateTask > TasksQueue
每帧执行一次的任务队列。
override void Overdraw(DrawContext dc)
void EnterText(string value, int index)
override void Draw(DrawContext dc)
static void SetCursorPosition(TextBoxWidget widget)
void ChangeTextNoEvent(string value)
void Delete(char? character=null, int count=1)
readonly PrimitivesRenderer2D PrimitivesRenderer2D
Widget RootWidget
virtual Widget HitTestGlobal(Vector2 point, Func< Widget, bool > predicate=null)
WidgetInput Input
Vector2 DesiredSize
bool IsDrawRequired
bool IsOverdrawRequired
Vector2 ActualSize
virtual Vector2 ScreenToWidget(Vector2 p)
Matrix GlobalTransform
static Color Red
static Color White
static Color DarkGray
定义 Color.cs:9
static Matrix CreateTranslation(float x, float y, float z)
static readonly Vector2 Zero
static readonly Vector2 UnitY
static Vector2 Transform(Vector2 v, Matrix m)
static readonly Vector2 UnitX