Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
TextBoxWidget.cs
1using System.Drawing;
8using SkiaSharp;
9
11
22public sealed class TextBoxWidget : WidgetBase
23{
24 private Color _backgroundColor = Color.FromArgb(255, 32, 32, 40);
25 private Color _borderColor = Color.FromArgb(255, 135, 135, 150);
26 private Color _focusedBorderColor = Color.FromArgb(255, 105, 155, 230);
27 private SKColor _textColor = SKColors.White;
28 private SKColor _placeholderColor = new(170, 170, 180);
29
30 private string _text = string.Empty;
31 private string _placeholder = string.Empty;
32 private int _caretIndex;
33 private int? _maxLength;
34 private double _initialKeyRepeatDelaySeconds = 1.25;
35 private double _repeatedKeyIntervalSeconds = 0.05;
36 private int? _repeatingKey;
37 private long _keyPressedTick;
38 private long _lastRepeatTick;
39 private Func<WidgetKeyboardEventArgs, char?> _characterResolver = ResolveWindowsVirtualKeyCharacter;
40
44 public event Action<string>? TextChanged;
45
49 public event Action<string>? Submitted;
50
60 public TextBoxWidget(RenderSurfaceHostBase renderSurfaceHost,
61 View view,
62 Rectangle bounds,
63 string text = "",
64 string? placeholder = null,
65 string? nickname = null)
66 : base(renderSurfaceHost, DirectDrawingMode.View, ValidateBounds(bounds).Location, nickname)
67 {
68 ArgumentNullException.ThrowIfNull(view);
69
70 _text = NormalizeText(text);
71 _placeholder = placeholder ?? string.Empty;
72 _caretIndex = _text.Length;
73
74 Background = CreateBackground(renderSurfaceHost, view, bounds);
75 TextBlock = CreateTextBlock(renderSurfaceHost, view, bounds);
76
79
80 CompleteInitialization();
81 }
82
92 public TextBoxWidget(RenderSurfaceHostBase renderSurfaceHost,
93 SceneLayer sceneLayer,
94 Rectangle bounds,
95 string text = "",
96 string? placeholder = null,
97 string? nickname = null)
98 : base(renderSurfaceHost, DirectDrawingMode.SceneLayer, ValidateBounds(bounds).Location, nickname)
99 {
100 ArgumentNullException.ThrowIfNull(sceneLayer);
101
102 _text = NormalizeText(text);
103 _placeholder = placeholder ?? string.Empty;
104 _caretIndex = _text.Length;
105
106 Background = CreateBackground(renderSurfaceHost, sceneLayer, bounds);
107 TextBlock = CreateTextBlock(renderSurfaceHost, sceneLayer, bounds);
108
110 Add(TextBlock);
111
112 CompleteInitialization();
113 }
114
119
123 public TextBlock TextBlock { get; }
124
128 public string Text
129 {
130 get => _text;
131 set => SetText(value);
132 }
133
137 public string Placeholder
138 {
139 get => _placeholder;
140 set
141 {
142 _placeholder = value ?? string.Empty;
143 RefreshDisplayedText();
144 }
145 }
146
150 public int CaretIndex
151 {
152 get => _caretIndex;
153 set
154 {
155 _caretIndex = Math.Clamp(value, 0, _text.Length);
156 RefreshDisplayedText();
157 }
158 }
159
163 public int? MaxLength
164 {
165 get => _maxLength;
166 set
167 {
168 if (value.HasValue && value.Value < 0)
169 throw new ArgumentOutOfRangeException(nameof(value));
170
171 _maxLength = value;
172
173 if (_maxLength.HasValue && _text.Length > _maxLength.Value)
174 SetText(_text[.._maxLength.Value]);
175 }
176 }
177
183 {
184 get => _initialKeyRepeatDelaySeconds;
185 set
186 {
187 if (!double.IsFinite(value) || value < 0d)
188 throw new ArgumentOutOfRangeException(nameof(value));
189
190 _initialKeyRepeatDelaySeconds = value;
191 }
192 }
193
200 {
201 get => _repeatedKeyIntervalSeconds;
202 set
203 {
204 if (!double.IsFinite(value) || value < 0d)
205 throw new ArgumentOutOfRangeException(nameof(value));
206
207 _repeatedKeyIntervalSeconds = value;
208 }
209 }
210
214 public bool IsReadOnly { get; set; }
215
219 public Rectangle Bounds => Mode == DirectDrawingMode.View
220 ? Background.ScreenBounds
221 : Background.WorldBounds;
222
226 public int SubmitKey { get; set; } = 13;
227
231 public int BackspaceKey { get; set; } = 8;
232
236 public int DeleteKey { get; set; } = 46;
237
241 public int LeftKey { get; set; } = 37;
242
246 public int RightKey { get; set; } = 39;
247
251 public int HomeKey { get; set; } = 36;
252
256 public int EndKey { get; set; } = 35;
257
261 public Func<WidgetKeyboardEventArgs, char?> CharacterResolver
262 {
263 get => _characterResolver;
264 set => _characterResolver = value ?? NoCharacterResolver;
265 }
266
272 public TextBoxWidget SetText(string? text)
273 {
274 string normalized = NormalizeText(text);
275
276 if (MaxLength.HasValue && normalized.Length > MaxLength.Value)
277 normalized = normalized[..MaxLength.Value];
278
279 if (_text == normalized)
280 {
281 _caretIndex = Math.Min(_caretIndex, _text.Length);
282 RefreshDisplayedText();
283 return this;
284 }
285
286 _text = normalized;
287 _caretIndex = _text.Length;
288 RefreshDisplayedText();
289 TextChanged?.Invoke(_text);
290 return this;
291 }
292
298 public TextBoxWidget InsertText(string? text)
299 {
300 if (IsReadOnly || string.IsNullOrEmpty(text))
301 return this;
302
303 string insertion = NormalizeText(text);
304 if (insertion.Length == 0)
305 return this;
306
307 if (MaxLength.HasValue)
308 {
309 int available = MaxLength.Value - _text.Length;
310 if (available <= 0)
311 return this;
312
313 if (insertion.Length > available)
314 insertion = insertion[..available];
315 }
316
317 _text = _text.Insert(_caretIndex, insertion);
318 _caretIndex += insertion.Length;
319 RefreshDisplayedText();
320 TextChanged?.Invoke(_text);
321 return this;
322 }
323
329 {
330 if (IsReadOnly || _caretIndex <= 0 || _text.Length == 0)
331 return this;
332
333 _text = _text.Remove(_caretIndex - 1, 1);
334 _caretIndex--;
335 RefreshDisplayedText();
336 TextChanged?.Invoke(_text);
337 return this;
338 }
339
345 {
346 if (IsReadOnly || _caretIndex < 0 || _caretIndex >= _text.Length)
347 return this;
348
349 _text = _text.Remove(_caretIndex, 1);
350 RefreshDisplayedText();
351 TextChanged?.Invoke(_text);
352 return this;
353 }
354
360 public TextBoxWidget MoveCaret(int delta)
361 {
362 CaretIndex = Math.Clamp(_caretIndex + delta, 0, _text.Length);
363 return this;
364 }
365
373 public TextBoxWidget SetColors(Color background,
374 Color border,
375 Color focusedBorder)
376 {
377 _backgroundColor = background;
378 _borderColor = border;
379 _focusedBorderColor = focusedBorder;
380
381 Background.SetColor(_backgroundColor)
382 .SetBorderColor(IsFocused ? _focusedBorderColor : _borderColor);
383 Refresh(Background);
384 return this;
385 }
386
393 public TextBoxWidget SetTextColors(SKColor text, SKColor placeholder)
394 {
395 _textColor = text;
396 _placeholderColor = placeholder;
397 RefreshDisplayedText();
398 return this;
399 }
400
407 {
408 Background.ZOrder = zOrder;
409 TextBlock.ZOrder = zOrder + 1;
410 return this;
411 }
412
414 protected override void OnPointerClick(WidgetPointerEventArgs args)
415 {
416 base.OnPointerClick(args);
417
418 if (!args.IsPrimaryButton)
419 return;
420
421 _caretIndex = _text.Length;
422 RefreshDisplayedText();
423 args.Handled = true;
424 }
425
427 protected override void OnFocusGained()
428 {
429 base.OnFocusGained();
430 Background.SetBorderColor(_focusedBorderColor);
431 Refresh(Background);
432 RefreshDisplayedText();
433 }
434
436 protected override void OnFocusLost()
437 {
438 base.OnFocusLost();
439 ResetKeyRepeatState();
440 Background.SetBorderColor(_borderColor);
441 Refresh(Background);
442 RefreshDisplayedText();
443 }
444
446 protected override void OnKeyboardInput(WidgetKeyboardEventArgs args)
447 {
448 base.OnKeyboardInput(args);
449
450 if (args.KeyAction == KeyAction.Released)
451 {
452 if (_repeatingKey == args.Key)
453 ResetKeyRepeatState();
454
455 return;
456 }
457
458 if (args.KeyAction is not KeyAction.Pressed and not KeyAction.Repeated)
459 return;
460
461 long currentTick = HighResTimer.GetCurrentTick();
462
463 if (args.KeyAction == KeyAction.Pressed)
464 {
465 _repeatingKey = args.Key;
466 _keyPressedTick = currentTick;
467 _lastRepeatTick = currentTick;
468 }
469 else
470 {
471 if (_repeatingKey != args.Key ||
472 _keyPressedTick == 0 ||
473 HighResTimer.GetDuration(_keyPressedTick, currentTick) < _initialKeyRepeatDelaySeconds ||
474 (_lastRepeatTick != 0 &&
475 HighResTimer.GetDuration(_lastRepeatTick, currentTick) < _repeatedKeyIntervalSeconds))
476 {
477 args.Handled = true;
478 return;
479 }
480
481 _lastRepeatTick = currentTick;
482 }
483
484 if (args.Key == SubmitKey)
485 {
486 if (args.KeyAction != KeyAction.Pressed)
487 return;
488
489 args.Handled = true;
490 Submitted?.Invoke(_text);
491 return;
492 }
493
494 if (args.Key == LeftKey)
495 {
496 args.Handled = true;
497 MoveCaret(-1);
498 return;
499 }
500
501 if (args.Key == RightKey)
502 {
503 args.Handled = true;
504 MoveCaret(1);
505 return;
506 }
507
508 if (args.Key == HomeKey)
509 {
510 args.Handled = true;
511 CaretIndex = 0;
512 return;
513 }
514
515 if (args.Key == EndKey)
516 {
517 args.Handled = true;
518 CaretIndex = _text.Length;
519 return;
520 }
521
522 if (args.Key == BackspaceKey)
523 {
524 args.Handled = true;
525 Backspace();
526 return;
527 }
528
529 if (args.Key == DeleteKey)
530 {
531 args.Handled = true;
532 Delete();
533 return;
534 }
535
536 if (IsReadOnly)
537 return;
538
539 char? character = CharacterResolver(args);
540 if (!character.HasValue)
541 return;
542
543 args.Handled = true;
544 InsertText(character.Value.ToString());
545 }
546
547 private void ResetKeyRepeatState()
548 {
549 _repeatingKey = null;
550 _keyPressedTick = 0;
551 _lastRepeatTick = 0;
552 }
553
554 private void CompleteInitialization()
555 {
556 CanReceiveFocus = true;
559 RefreshDisplayedText();
560 }
561
562 private void RefreshDisplayedText()
563 {
564 if (IsFocused)
565 {
566 string withCaret = _text.Insert(_caretIndex, "|");
567 TextBlock.SetText(withCaret)
568 .SetColors(_textColor, SKColors.Transparent);
569 return;
570 }
571
572 bool showPlaceholder = _text.Length == 0 && _placeholder.Length > 0;
573 TextBlock.SetText(showPlaceholder ? _placeholder : _text)
574 .SetColors(showPlaceholder ? _placeholderColor : _textColor, SKColors.Transparent);
575 }
576
577 private DirectRectangle CreateBackground(RenderSurfaceHostBase host,
578 View view,
579 Rectangle bounds)
580 {
581 return ConfigureBackground(new DirectRectangle(_backgroundColor, host, view, bounds));
582 }
583
584 private DirectRectangle CreateBackground(RenderSurfaceHostBase host,
585 SceneLayer layer,
586 Rectangle bounds)
587 {
588 return ConfigureBackground(new DirectRectangle(_backgroundColor, host, layer, bounds));
589 }
590
591 private DirectRectangle ConfigureBackground(DirectRectangle background)
592 {
593 return background.SetFilled(true)
594 .SetBorderColor(_borderColor)
595 .SetStrokeWidth(1.5f)
596 .SetCornerRadius(4f);
597 }
598
599 private static TextBlock CreateTextBlock(RenderSurfaceHostBase host,
600 View view,
601 Rectangle bounds)
602 {
603 return ConfigureTextBlock(new TextBlock(host, view, bounds));
604 }
605
606 private static TextBlock CreateTextBlock(RenderSurfaceHostBase host,
607 SceneLayer layer,
608 Rectangle bounds)
609 {
610 return ConfigureTextBlock(new TextBlock(host, layer, view: null, worldBounds: bounds));
611 }
612
613 private static TextBlock ConfigureTextBlock(TextBlock textBlock)
614 {
615 return textBlock.SetFont(SKTypeface.Default, 16f, minSize: 10f)
616 .SetColors(SKColors.White, SKColors.Transparent)
617 .SetAlignment(SKTextAlign.Left, TextBlock.VerticalAlign.Center)
618 .SetPadding(8f, 0f)
619 .EnableWrapping(false);
620 }
621
622 private static string NormalizeText(string? text)
623 {
624 return (text ?? string.Empty)
625 .Replace('\r', ' ')
626 .Replace('\n', ' ');
627 }
628
629 private static char? NoCharacterResolver(WidgetKeyboardEventArgs args)
630 {
631 return null;
632 }
633
634 private static char? ResolveWindowsVirtualKeyCharacter(WidgetKeyboardEventArgs args)
635 {
636 if ((args.Modifiers & (KeyboardModifierState.Ctrl | KeyboardModifierState.Alt)) != 0)
637 return null;
638
639 bool shift = (args.Modifiers & KeyboardModifierState.Shift) != 0;
640 int key = args.Key;
641
642 if (key is >= 65 and <= 90)
643 {
644 char letter = (char)key;
645 return shift ? letter : char.ToLowerInvariant(letter);
646 }
647
648 if (key is >= 48 and <= 57)
649 {
650 if (!shift)
651 return (char)key;
652
653 const string shiftedDigits = ")!@#$%^&*(";
654 return shiftedDigits[key - 48];
655 }
656
657 if (key is >= 96 and <= 105)
658 return (char)('0' + key - 96);
659
660 return key switch
661 {
662 32 => ' ',
663 106 => '*',
664 107 => '+',
665 109 => '-',
666 110 => '.',
667 111 => '/',
668 186 => shift ? ':' : ';',
669 187 => shift ? '+' : '=',
670 188 => shift ? '<' : ',',
671 189 => shift ? '_' : '-',
672 190 => shift ? '>' : '.',
673 191 => shift ? '?' : '/',
674 192 => shift ? '~' : '`',
675 219 => shift ? '{' : '[',
676 220 => shift ? '|' : '\\',
677 221 => shift ? '}' : ']',
678 222 => shift ? '"' : '\'',
679 _ => null
680 };
681 }
682
683 private static void Refresh(DirectRectangle rectangle)
684 {
685 rectangle.SetPosition(rectangle.GetPosition());
686 }
687
688 private static Rectangle ValidateBounds(Rectangle bounds)
689 {
690 if (bounds.Width < 40 || bounds.Height < 18)
691 throw new ArgumentOutOfRangeException(nameof(bounds), bounds, "Text-box bounds are too small.");
692
693 return bounds;
694 }
695}
virtual DirectComposite Remove(IDirectCompositeChild child)
Removes a child without disposing it.
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
View? View
Gets the view shared by all children in view mode.
virtual DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
int ZOrder
Gets or sets the Z-order (depth) used for sorting this direct drawing relative to other drawables.
Vector2 GetPosition()
Gets the current position of this direct drawing in pixel coordinates.
void SetPosition(Vector2 p)
Sets the position of this direct drawing to the specified pixel coordinates.
Renders a configurable rectangle with support for fill, border, rounded corners, dash patterns,...
DirectRectangle SetFilled(bool isFilled)
Sets whether the rectangle interior is filled with the base color (or pattern).
DirectRectangle SetBorderColor(Color color)
Sets a distinct border (stroke) color, independent of the fill color.
DirectRectangle SetStrokeWidth(float width)
Sets the width (thickness) of the rectangle's stroke (outline) in pixels.
DirectRectangle SetCornerRadius(float radius)
Sets the radius for rounded corners in pixels.
A retained-mode text drawable with wrapping, multi-line ( ) support, horizontal/vertical alignment,...
Definition TextBlock.cs:44
TextBlock SetFont(SKTypeface typeface, float size, float? minSize=null)
Sets the font face and size (and optional minimum size for auto-shrink).
Definition TextBlock.cs:394
TextBlock EnableWrapping(bool enable=true)
Enables or disables word wrapping when laying out paragraphs.
Definition TextBlock.cs:503
TextBlock SetAlignment(SKTextAlign h, VerticalAlign v)
Sets the horizontal and vertical alignment used when drawing the laid-out lines within the bounds.
Definition TextBlock.cs:433
TextBlock SetPadding(float horizontal, float vertical)
Sets symmetric horizontal and vertical padding and invalidates cached layout.
Definition TextBlock.cs:317
TextBlock SetColors(SKColor fg, SKColor bg)
Sets the foreground (text) and background colors using Skia colors.
Definition TextBlock.cs:410
TextBlock SetText(string text)
Sets the current text content and rebuilds layout as needed. If no reveal animation is active,...
Definition TextBlock.cs:358
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
override void OnPointerClick(WidgetPointerEventArgs args)
Called when a pointer click is dispatched to the widget.
Action< string >? TextChanged
Occurs when the text changes.
TextBoxWidget InsertText(string? text)
Inserts text at the current caret position.
double InitialKeyRepeatDelaySeconds
Gets or sets the delay, in seconds, before a held key begins repeating. Pressed events remain immedia...
override void OnFocusLost()
Called when the widget loses focus.
TextBoxWidget SetText(string? text)
Replaces the text and moves the caret to the end.
Func< WidgetKeyboardEventArgs, char?> CharacterResolver
Gets or sets the function used to convert routed key events into printable characters.
TextBoxWidget SetTextColors(SKColor text, SKColor placeholder)
Sets the text and placeholder colors.
TextBoxWidget MoveCaret(int delta)
Moves the caret by the specified character delta.
override void OnKeyboardInput(WidgetKeyboardEventArgs args)
Called when keyboard input is routed to the widget.
TextBoxWidget Backspace()
Removes the character before the caret when possible.
string Placeholder
Gets or sets placeholder text shown while the control is empty and unfocused.
int RightKey
Gets or sets the key code used to move right. Defaults to 39.
string Text
Gets or sets the current text.
int LeftKey
Gets or sets the key code used to move left. Defaults to 37.
TextBoxWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle bounds, string text="", string? placeholder=null, string? nickname=null)
Creates a view-level text box.
Action< string >? Submitted
Occurs when the configured submit key is pressed.
TextBoxWidget Delete()
Removes the character at the caret when possible.
bool IsReadOnly
Gets or sets whether user editing is disabled.
TextBoxWidget SetColors(Color background, Color border, Color focusedBorder)
Sets the background and border colors used for normal and focused states.
int EndKey
Gets or sets the key code used to move to the end. Defaults to 35.
DirectRectangle Background
Gets the text-box background and border drawing.
int BackspaceKey
Gets or sets the key code used for backspace. Defaults to 8.
TextBlock TextBlock
Gets the text drawing used by the text box.
override void OnFocusGained()
Called when the widget gains focus.
Rectangle Bounds
Gets the text-box bounds in its native coordinate space.
int? MaxLength
Gets or sets the maximum permitted text length. A null value means unlimited.
int DeleteKey
Gets or sets the key code used for Delete. Defaults to 46.
int SubmitKey
Gets or sets the key code used to submit the text. Defaults to Enter (13).
double RepeatedKeyIntervalSeconds
Gets or sets the minimum interval, in seconds, between repeated held-key actions after InitialKeyRepe...
TextBoxWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle bounds, string text="", string? placeholder=null, string? nickname=null)
Creates a scene-layer text box.
int CaretIndex
Gets or sets the caret position in the range zero through Text length.
int HomeKey
Gets or sets the key code used to move to the beginning. Defaults to 36.
TextBoxWidget SetTextBoxZOrder(int zOrder)
Sets the base Z-order used by the text-box visuals.
bool CanReceiveFocus
Gets or sets a value indicating whether the widget can receive keyboard focus.
WidgetBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the WidgetBase class.
Definition WidgetBase.cs:95
bool IsFocused
Gets a value indicating whether the widget currently has keyboard focus.
bool IsKeyboardInputEnabled
Gets or sets a value indicating whether the widget can receive keyboard input.
Provides data for widget keyboard input events routed by WidgetInputRouter.
int Key
Gets the integer key identifier.
KeyAction KeyAction
Gets the action associated with this key event.
Event arguments for pointer-based widget callbacks.
bool IsPrimaryButton
Gets whether the pointer event represents a primary-button interaction.
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ View
Performs direct drawing at the view level using screen-space coordinates.
KeyboardModifierState
Represents the state of keyboard modifier keys (Shift, Ctrl, Alt) that can be pressed simultaneously ...
KeyAction
Represents the type of action performed on a keyboard key, indicating whether the key was pressed,...
Definition KeyAction.cs:10