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
TabControlWidget.cs
1using System.Drawing;
2using System.Numerics;
8using SkiaSharp;
9
11
23public sealed class TabControlWidget : ContainerWidget, IWidgetKeyboardFallback
24{
25 private const int DefaultTabHeight = 32;
26 private const int DefaultPreferredTabWidth = 132;
27 private const float DefaultReorderDragThresholdPx = 6f;
28
29 private readonly List<TabPageWidget> _tabs = [];
30 private readonly Dictionary<TabPageWidget, TabHeaderWidget> _headers = [];
31
32 private readonly View? _view;
33 private readonly SceneLayer? _sceneLayer;
34
35 private Size _size;
36 private int _selectedIndex = -1;
37 private int _tabRowCount = 1;
38 private int _displayedRowCount = 1;
39 private int _displayedColumnCount = 1;
40 private int _tabHeight = DefaultTabHeight;
41 private int _preferredTabWidth = DefaultPreferredTabWidth;
42 private int _baseZOrder;
43 private float _tabReorderDragThresholdPx = DefaultReorderDragThresholdPx;
44 private bool _autoExpandRows;
45 private bool _isTabReorderingEnabled;
46 private bool _isShown;
47 private bool _disposed;
48
49 private Color _normalTabColor = Color.FromArgb(255, 60, 60, 68);
50 private Color _hoverTabColor = Color.FromArgb(255, 78, 78, 88);
51 private Color _pressedTabColor = Color.FromArgb(255, 42, 42, 48);
52 private Color _selectedTabColor = Color.FromArgb(255, 48, 89, 142);
53 private Color _tabTextColor = Color.White;
54
58 public event Action<TabPageWidget>? SelectedTabChanged;
59
63 public event Action<TabPageWidget, int, int>? TabReordered;
64
72 public TabControlWidget(RenderSurfaceHostBase renderSurfaceHost,
73 View view,
74 Rectangle bounds,
75 string? nickname = null)
76 : base(renderSurfaceHost, DirectDrawingMode.View, ValidateBounds(bounds).Location, nickname)
77 {
78 ArgumentNullException.ThrowIfNull(view);
79
80 _view = view;
81 _size = bounds.Size;
82
83 InitializeInput();
84 }
85
93 public TabControlWidget(RenderSurfaceHostBase renderSurfaceHost,
94 SceneLayer sceneLayer,
95 Rectangle bounds,
96 string? nickname = null)
97 : base(renderSurfaceHost, DirectDrawingMode.SceneLayer, ValidateBounds(bounds).Location, nickname)
98 {
99 ArgumentNullException.ThrowIfNull(sceneLayer);
100
101 _sceneLayer = sceneLayer;
102 _size = bounds.Size;
103
104 InitializeInput();
105 }
106
110 public Rectangle Bounds
111 {
112 get
113 {
114 Vector2 position = GetPosition();
115 return new Rectangle(
116 (int)MathF.Round(position.X),
117 (int)MathF.Round(position.Y),
118 _size.Width,
119 _size.Height);
120 }
121 }
122
126 public Size Size
127 {
128 get => _size;
129 set
130 {
131 ValidateSize(value);
132
133 if (_size == value)
134 return;
135
136 _size = value;
137 Relayout();
138 }
139 }
140
144 public IReadOnlyList<TabPageWidget> Tabs => _tabs.AsReadOnly();
145
149 public int SelectedIndex => _selectedIndex;
150
154 public TabPageWidget? SelectedTab => _selectedIndex >= 0 && _selectedIndex < _tabs.Count
155 ? _tabs[_selectedIndex]
156 : null;
157
161 public int TabHeight
162 {
163 get => _tabHeight;
164 set
165 {
166 if (value < 20)
167 throw new ArgumentOutOfRangeException(nameof(value), "The tab height must be at least 20 pixels.");
168
169 if (_tabHeight == value)
170 return;
171
172 _tabHeight = value;
173 Relayout();
174 }
175 }
176
184 public int TabRowCount
185 {
186 get => _tabRowCount;
187 set
188 {
189 if (value < 1)
190 throw new ArgumentOutOfRangeException(nameof(value), "At least one tab row is required.");
191
192 if (_tabRowCount == value)
193 return;
194
195 _tabRowCount = value;
196 Relayout();
197 }
198 }
199
208 public bool AutoExpandRows
209 {
210 get => _autoExpandRows;
211 set
212 {
213 if (_autoExpandRows == value)
214 return;
215
216 _autoExpandRows = value;
217 Relayout();
218 }
219 }
220
225 {
226 get => _preferredTabWidth;
227 set
228 {
229 if (value < 48)
230 throw new ArgumentOutOfRangeException(nameof(value), "The preferred tab width must be at least 48 pixels.");
231
232 if (_preferredTabWidth == value)
233 return;
234
235 _preferredTabWidth = value;
236 Relayout();
237 }
238 }
239
243 public int DisplayedTabRowCount => _displayedRowCount;
244
248 public int DisplayedTabColumnCount => _displayedColumnCount;
249
254 {
255 get => _isTabReorderingEnabled;
256 set
257 {
258 if (_isTabReorderingEnabled == value)
259 return;
260
261 _isTabReorderingEnabled = value;
262
263 foreach (TabHeaderWidget header in _headers.Values)
264 header.IsDragEnabled = value;
265 }
266 }
267
272 {
273 get => _tabReorderDragThresholdPx;
274 set
275 {
276 if (value < 0f)
277 throw new ArgumentOutOfRangeException(nameof(value), "The tab reordering threshold cannot be negative.");
278
279 if (Math.Abs(_tabReorderDragThresholdPx - value) < float.Epsilon)
280 return;
281
282 _tabReorderDragThresholdPx = value;
283
284 foreach (TabHeaderWidget header in _headers.Values)
285 header.DragThresholdPx = value;
286 }
287 }
288
296 public TabPageWidget AddTab(string title,
297 char? mnemonic = null,
298 string? nickname = null)
299 {
300 ObjectDisposedException.ThrowIf(_disposed, this);
301 ValidateTitle(title);
302 ValidateMnemonic(mnemonic);
303
304 Rectangle initialHeaderBounds = new(Bounds.Location, new Size(1, TabHeight));
305 Rectangle initialContentBounds = new(
306 Bounds.Left,
307 Bounds.Top + TabHeight,
308 Bounds.Width,
309 Math.Max(1, Bounds.Height - TabHeight));
310
312 ? new TabPageWidget(RenderSurfaceHost, _view!, initialContentBounds, title, mnemonic, nickname)
313 : new TabPageWidget(RenderSurfaceHost, _sceneLayer!, initialContentBounds, title, mnemonic, nickname);
314
315 TabHeaderWidget header = Mode == DirectDrawingMode.View
316 ? new TabHeaderWidget(RenderSurfaceHost, _view!, initialHeaderBounds, title, $"{page.Nickname}.header")
317 : new TabHeaderWidget(RenderSurfaceHost, _sceneLayer!, initialHeaderBounds, title, $"{page.Nickname}.header");
318
319 page.TitleChanged += OnPageTitleChanged;
320 page.Disposing += OnPageDisposing;
321 header.Invoked += OnHeaderInvoked;
322 header.ReorderDragged += OnHeaderDragged;
323 header.ReorderDragEnded += OnHeaderDragEnded;
324 header.Disposing += OnHeaderDisposing;
325 header.IsDragEnabled = IsTabReorderingEnabled;
326 header.DragThresholdPx = TabReorderDragThresholdPx;
327
328 _tabs.Add(page);
329 _headers.Add(page, header);
330
331 Add(page, Vector2.Zero);
332 Add(header, Vector2.Zero);
333
334 if (_selectedIndex < 0)
335 _selectedIndex = 0;
336
337 Relayout();
338 UpdateSelectedTabVisuals();
339 UpdatePageVisibility();
340
341 return page;
342 }
343
350 public bool RemoveTab(TabPageWidget page,
351 bool dispose = false)
352 {
353 ArgumentNullException.ThrowIfNull(page);
354 return RemoveTabCore(page, dispose, raiseSelectionChanged: true);
355 }
356
362 public TabControlWidget SelectTab(int index)
363 {
364 ObjectDisposedException.ThrowIf(_disposed, this);
365
366 if (index < 0 || index >= _tabs.Count)
367 throw new ArgumentOutOfRangeException(nameof(index));
368
369 if (_selectedIndex == index)
370 return this;
371
372 _selectedIndex = index;
373 UpdateSelectedTabVisuals();
374 UpdatePageVisibility();
375
376 SelectedTabChanged?.Invoke(_tabs[index]);
377 return this;
378 }
379
386 {
387 ArgumentNullException.ThrowIfNull(page);
388
389 int index = _tabs.IndexOf(page);
390 if (index < 0)
391 throw new ArgumentException("The page is not owned by this tab control.", nameof(page));
392
393 return SelectTab(index);
394 }
395
402 public TabControlWidget MoveTab(int fromIndex,
403 int toIndex)
404 {
405 ObjectDisposedException.ThrowIf(_disposed, this);
406
407 if (fromIndex < 0 || fromIndex >= _tabs.Count)
408 throw new ArgumentOutOfRangeException(nameof(fromIndex));
409
410 if (toIndex < 0 || toIndex >= _tabs.Count)
411 throw new ArgumentOutOfRangeException(nameof(toIndex));
412
413 if (fromIndex == toIndex)
414 return this;
415
416 TabPageWidget page = _tabs[fromIndex];
417 TabPageWidget? selectedPage = SelectedTab;
418
419 _tabs.RemoveAt(fromIndex);
420 _tabs.Insert(toIndex, page);
421 _selectedIndex = selectedPage is null ? -1 : _tabs.IndexOf(selectedPage);
422
423 Relayout();
424 TabReordered?.Invoke(page, fromIndex, toIndex);
425
426 return this;
427 }
428
436 int toIndex)
437 {
438 ArgumentNullException.ThrowIfNull(page);
439
440 int fromIndex = _tabs.IndexOf(page);
441 if (fromIndex < 0)
442 throw new ArgumentException("The page is not owned by this tab control.", nameof(page));
443
444 return MoveTab(fromIndex, toIndex);
445 }
446
452 {
453 ObjectDisposedException.ThrowIf(_disposed, this);
454
455 Rectangle bounds = Bounds;
456 (_displayedRowCount, _displayedColumnCount) = ResolveGrid(bounds);
457 Rectangle contentBounds = CreateContentBounds(bounds, _displayedRowCount);
458
459 for (int index = 0; index < _tabs.Count; index++)
460 {
461 TabPageWidget page = _tabs[index];
462 TabHeaderWidget header = _headers[page];
463 Rectangle headerBounds = CreateHeaderBounds(bounds, index, _displayedColumnCount);
464
465 header.SetBounds(headerBounds);
466 page.HeaderBounds = headerBounds;
467 page.SetContentBounds(contentBounds);
468
469 SetLocalOffset(header, new Vector2(headerBounds.Left - bounds.Left, headerBounds.Top - bounds.Top));
470 SetLocalOffset(page, new Vector2(contentBounds.Left - bounds.Left, contentBounds.Top - bounds.Top));
471 }
472
473 ApplyZOrder();
474 return this;
475 }
476
485 public TabControlWidget SetTabColors(Color normal,
486 Color hover,
487 Color pressed,
488 Color selected)
489 {
490 _normalTabColor = normal;
491 _hoverTabColor = hover;
492 _pressedTabColor = pressed;
493 _selectedTabColor = selected;
494 UpdateSelectedTabVisuals();
495
496 return this;
497 }
498
505 {
506 _tabTextColor = color;
507
508 foreach (TabHeaderWidget header in _headers.Values)
509 header.SetTextColor(color);
510
511 return this;
512 }
513
520 {
521 _baseZOrder = zOrder;
522 ApplyZOrder();
523
524 return this;
525 }
526
528 public override void Dispose()
529 {
530 if (_disposed)
531 return;
532
533 _disposed = true;
534
535 foreach (TabPageWidget page in _tabs)
536 {
537 page.TitleChanged -= OnPageTitleChanged;
538 page.Disposing -= OnPageDisposing;
539
540 TabHeaderWidget header = _headers[page];
541 header.Invoked -= OnHeaderInvoked;
542 header.ReorderDragged -= OnHeaderDragged;
543 header.ReorderDragEnded -= OnHeaderDragEnded;
544 header.Disposing -= OnHeaderDisposing;
545 }
546
547 base.Dispose();
548
549 _tabs.Clear();
550 _headers.Clear();
551 SelectedTabChanged = null;
552 TabReordered = null;
553 }
554
556 protected override void ProcessShown()
557 {
558 base.ProcessShown();
559 _isShown = true;
560 UpdatePageVisibility();
561 }
562
564 protected override void ProcessHidden()
565 {
566 _isShown = false;
567 base.ProcessHidden();
568 }
569
571 protected override void ProcessActivated()
572 {
573 base.ProcessActivated();
574
575 SelectedTab?.Activate();
576 }
577
579 protected override void OnKeyboardInput(WidgetKeyboardEventArgs args)
580 {
581 base.OnKeyboardInput(args);
582 HandleKeyboardInput(args);
583 }
584
586 void IWidgetKeyboardFallback.HandleUnhandledKeyboardInput(WidgetKeyboardEventArgs args)
587 {
588 HandleKeyboardInput(args);
589 }
590
591 private void InitializeInput()
592 {
593 IsPointerInputEnabled = false;
595 CanReceiveFocus = false;
596 }
597
598 private bool RemoveTabCore(TabPageWidget page,
599 bool dispose,
600 bool raiseSelectionChanged)
601 {
602 int removedIndex = _tabs.IndexOf(page);
603 if (removedIndex < 0)
604 return false;
605
606 TabPageWidget? previouslySelected = SelectedTab;
607 TabHeaderWidget header = _headers[page];
608
609 page.TitleChanged -= OnPageTitleChanged;
610 page.Disposing -= OnPageDisposing;
611 header.Invoked -= OnHeaderInvoked;
612 header.ReorderDragged -= OnHeaderDragged;
613 header.ReorderDragEnded -= OnHeaderDragEnded;
614 header.Disposing -= OnHeaderDisposing;
615
616 _tabs.RemoveAt(removedIndex);
617 _headers.Remove(page);
618
619 Remove(header);
620 Remove(page);
621
622 header.Dispose();
623
624 if (dispose)
625 page.Dispose();
626
627 if (_tabs.Count == 0)
628 {
629 _selectedIndex = -1;
630 }
631 else if (ReferenceEquals(previouslySelected, page))
632 {
633 _selectedIndex = Math.Min(removedIndex, _tabs.Count - 1);
634 }
635 else
636 {
637 _selectedIndex = previouslySelected is null ? 0 : _tabs.IndexOf(previouslySelected);
638 }
639
640 Relayout();
641 UpdateSelectedTabVisuals();
642 UpdatePageVisibility();
643
644 if (raiseSelectionChanged && SelectedTab is not null && !ReferenceEquals(previouslySelected, SelectedTab))
646
647 return true;
648 }
649
650 private void OnPageTitleChanged(TabPageWidget page)
651 {
652 if (_headers.TryGetValue(page, out TabHeaderWidget? header))
653 header.SetText(page.Title);
654 }
655
656 private void OnPageDisposing(object? sender,
657 IDirectDrawable _)
658 {
659 if (!_disposed && sender is TabPageWidget page)
660 RemoveTabCore(page, dispose: true, raiseSelectionChanged: true);
661 }
662
663 private void OnHeaderDisposing(object? sender,
664 IDirectDrawable _)
665 {
666 if (_disposed || sender is not TabHeaderWidget header)
667 return;
668
669 TabPageWidget? page = _headers.FirstOrDefault(pair => ReferenceEquals(pair.Value, header)).Key;
670 if (page is not null)
671 RemoveTabCore(page, dispose: true, raiseSelectionChanged: true);
672 }
673
674 private void OnHeaderInvoked(TabHeaderWidget header)
675 {
676 TabPageWidget? page = _headers.FirstOrDefault(pair => ReferenceEquals(pair.Value, header)).Key;
677 if (page is not null)
678 SelectTab(page);
679 }
680
681 private void OnHeaderDragged(TabHeaderWidget header,
682 WidgetDragEventArgs args,
683 View view)
684 {
686 return;
687
688 TabPageWidget? page = _headers.FirstOrDefault(pair => ReferenceEquals(pair.Value, header)).Key;
689 if (page is null)
690 return;
691
692 int fromIndex = _tabs.IndexOf(page);
693 int targetIndex = GetHeaderIndexAt(args.CurrentScreenPositionPx, view);
694
695 if (targetIndex >= 0 && targetIndex != fromIndex)
696 MoveTab(fromIndex, targetIndex);
697 }
698
699 private void OnHeaderDragEnded(TabHeaderWidget _)
700 {
701 Relayout();
702 }
703
704 private int GetHeaderIndexAt(PointF screenPositionPx,
705 View view)
706 {
707 Point screenPosition = Point.Round(screenPositionPx);
708
709 for (int index = 0; index < _tabs.Count; index++)
710 {
711 TabHeaderWidget header = _headers[_tabs[index]];
712 if (header.HitTest(view, screenPosition))
713 {
714 return index;
715 }
716 }
717
718 return -1;
719 }
720
721 private void HandleKeyboardInput(WidgetKeyboardEventArgs args)
722 {
723 if (args.Handled || args.KeyAction != KeyAction.Pressed || args.Modifiers != KeyboardModifierState.Alt)
724 return;
725
726 for (int index = 0; index < _tabs.Count; index++)
727 {
728 if (!MatchesMnemonic(_tabs[index].Mnemonic, args.Key))
729 continue;
730
731 args.Handled = true;
732 SelectTab(index);
733 return;
734 }
735 }
736
737 private void UpdateSelectedTabVisuals()
738 {
739 for (int index = 0; index < _tabs.Count; index++)
740 {
741 TabHeaderWidget header = _headers[_tabs[index]];
742 bool selected = index == _selectedIndex;
743
744 header.SetColors(
745 selected ? _selectedTabColor : _normalTabColor,
746 selected ? _selectedTabColor : _hoverTabColor,
747 selected ? _selectedTabColor : _pressedTabColor);
748 header.SetTextColor(_tabTextColor);
749 }
750 }
751
752 private void UpdatePageVisibility()
753 {
754 for (int index = 0; index < _tabs.Count; index++)
755 {
756 TabPageWidget page = _tabs[index];
757 bool isSelected = index == _selectedIndex;
758
759 if (_isShown && isSelected)
760 {
761 page.Show();
762 page.Activate();
763 }
764 else if (!isSelected || _isShown)
765 {
766 page.Hide();
767 }
768 }
769 }
770
771 private void ApplyZOrder()
772 {
773 foreach (TabPageWidget page in _tabs)
774 {
775 page.SetPageZOrder(_baseZOrder);
776 _headers[page].SetHeaderZOrder(_baseZOrder + 100);
777 }
778 }
779
780 private (int Rows, int Columns) ResolveGrid(Rectangle bounds)
781 {
782 if (!AutoExpandRows)
783 {
784 int columns = Math.Max(1, (int)Math.Ceiling(_tabs.Count / (double)TabRowCount));
785 return (TabRowCount, columns);
786 }
787
788 int autoColumns = _tabs.Count == 0
789 ? 1
790 : Math.Min(_tabs.Count, Math.Max(1, bounds.Width / PreferredTabWidth));
791 int autoRows = Math.Max(1, (int)Math.Ceiling(_tabs.Count / (double)autoColumns));
792
793 return (autoRows, autoColumns);
794 }
795
796 private Rectangle CreateHeaderBounds(Rectangle bounds,
797 int index,
798 int columns)
799 {
800 int row = index / columns;
801 int column = index % columns;
802 int left = bounds.Left + column * bounds.Width / columns;
803 int right = bounds.Left + (column + 1) * bounds.Width / columns;
804
805 return Rectangle.FromLTRB(
806 left,
807 bounds.Top + row * TabHeight,
808 right,
809 bounds.Top + (row + 1) * TabHeight);
810 }
811
812 private Rectangle CreateContentBounds(Rectangle bounds,
813 int rowCount)
814 {
815 int contentTop = bounds.Top + rowCount * TabHeight;
816
817 return new Rectangle(
818 bounds.Left,
819 contentTop,
820 bounds.Width,
821 Math.Max(1, bounds.Bottom - contentTop));
822 }
823
824 private static Rectangle ValidateBounds(Rectangle bounds)
825 {
826 ValidateSize(bounds.Size);
827 return bounds;
828 }
829
830 private static void ValidateSize(Size size)
831 {
832 if (size.Width <= 0 || size.Height <= DefaultTabHeight)
833 {
834 throw new ArgumentOutOfRangeException(
835 nameof(size),
836 size,
837 "Tab-control bounds must be positive and taller than one tab row.");
838 }
839 }
840
841 private void ValidateMnemonic(char? mnemonic)
842 {
843 if (mnemonic is not char value)
844 return;
845
846 if (_tabs.Any(tab => tab.Mnemonic is char existing &&
847 char.ToUpperInvariant(existing) == char.ToUpperInvariant(value)))
848 {
849 throw new ArgumentException($"Duplicate tab mnemonic '{value}'.", nameof(mnemonic));
850 }
851 }
852
853 private static void ValidateTitle(string title)
854 {
855 ArgumentException.ThrowIfNullOrWhiteSpace(title);
856 }
857
858 private static bool MatchesMnemonic(char? mnemonic,
859 int key)
860 {
861 return mnemonic is char character &&
862 key >= 0 &&
863 key <= char.MaxValue &&
864 char.ToUpperInvariant(character) == char.ToUpperInvariant((char)key);
865 }
866
867 private sealed class TabHeaderWidget : DraggableWidgetBase
868 {
869 private Color _normalColor = Color.FromArgb(255, 60, 60, 68);
870 private Color _hoverColor = Color.FromArgb(255, 78, 78, 88);
871 private Color _pressedColor = Color.FromArgb(255, 42, 42, 48);
872
876 internal event Action<TabHeaderWidget>? Invoked;
877
881 internal event Action<TabHeaderWidget, WidgetDragEventArgs, View>? ReorderDragged;
882
886 internal event Action<TabHeaderWidget>? ReorderDragEnded;
887
888 private View? _dragView;
889 private bool _disposed;
890
899 internal TabHeaderWidget(RenderSurfaceHostBase renderSurfaceHost,
900 View view,
901 Rectangle bounds,
902 string text,
903 string nickname)
904 : base(renderSurfaceHost, DirectDrawingMode.View, bounds.Location, nickname)
905 {
906 ArgumentNullException.ThrowIfNull(view);
907
908 Background = CreateBackground(renderSurfaceHost, view, bounds, nickname);
909 Label = CreateLabel(renderSurfaceHost, view, bounds, text);
910 CompleteInitialization();
911 }
912
921 internal TabHeaderWidget(RenderSurfaceHostBase renderSurfaceHost,
922 SceneLayer sceneLayer,
923 Rectangle bounds,
924 string text,
925 string nickname)
926 : base(renderSurfaceHost, DirectDrawingMode.SceneLayer, bounds.Location, nickname)
927 {
928 ArgumentNullException.ThrowIfNull(sceneLayer);
929
930 Background = CreateBackground(renderSurfaceHost, sceneLayer, bounds, nickname);
931 Label = CreateLabel(renderSurfaceHost, sceneLayer, bounds, text);
932 CompleteInitialization();
933 }
934
938 internal DirectRectangle Background { get; }
939
943 internal TextBlock Label { get; }
944
948 internal Rectangle Bounds => Mode == DirectDrawingMode.View
949 ? Background.ScreenBounds
950 : Background.WorldBounds;
951
956 internal void SetBounds(Rectangle bounds)
957 {
958 if (bounds.Width <= 0 || bounds.Height <= 0)
959 throw new ArgumentOutOfRangeException(nameof(bounds));
960
961 if (Mode == DirectDrawingMode.View)
962 {
963 Background.ScreenBounds = bounds;
964 Label.ScreenBounds = bounds;
965 }
966 else
967 {
968 Background.WorldBounds = bounds;
969 Label.WorldBounds = bounds;
970 }
971
972 SetPosition(bounds.Location.X, bounds.Location.Y);
973 }
974
979 internal void SetText(string text)
980 {
981 Label.SetText(text);
982 }
983
990 internal void SetColors(Color normal,
991 Color hover,
992 Color pressed)
993 {
994 _normalColor = normal;
995 _hoverColor = hover;
996 _pressedColor = pressed;
997 UpdateBackgroundColor(normal);
998 }
999
1004 internal void SetTextColor(Color color)
1005 {
1006 Label.SetColors(color, Color.Transparent);
1007 }
1008
1013 internal void SetHeaderZOrder(int zOrder)
1014 {
1015 Background.ZOrder = zOrder;
1016 Label.ZOrder = zOrder + 1;
1017 }
1018
1020 protected override Vector2 ConvertScreenDeltaToPositionDelta(Vector2 totalScreenDeltaPx,
1021 View view)
1022 {
1023 return Vector2.Zero;
1024 }
1025
1027 protected override void OnPointerEnter(WidgetPointerEventArgs args)
1028 {
1029 base.OnPointerEnter(args);
1030 UpdateBackgroundColor(_hoverColor);
1031 }
1032
1034 protected override void OnPointerLeave(WidgetPointerEventArgs args)
1035 {
1036 base.OnPointerLeave(args);
1037 UpdateBackgroundColor(_normalColor);
1038 }
1039
1041 protected override void OnPointerDown(WidgetPointerEventArgs args)
1042 {
1043 base.OnPointerDown(args);
1044
1045 if (args.IsPrimaryButton)
1046 {
1047 _dragView = args.View;
1048 UpdateBackgroundColor(_pressedColor);
1049 }
1050 }
1051
1053 protected override void OnPointerUp(WidgetPointerEventArgs args)
1054 {
1055 base.OnPointerUp(args);
1056 UpdateBackgroundColor(_hoverColor);
1057 }
1058
1060 protected override void OnPointerClick(WidgetPointerEventArgs args)
1061 {
1062 base.OnPointerClick(args);
1063
1064 if (!args.IsPrimaryButton)
1065 return;
1066
1067 args.Handled = true;
1068 Invoked?.Invoke(this);
1069 }
1070
1072 protected override void OnKeyboardInput(WidgetKeyboardEventArgs args)
1073 {
1074 base.OnKeyboardInput(args);
1075
1076 if (args.KeyAction != KeyAction.Pressed || args.Key is not 13 and not 32)
1077 return;
1078
1079 args.Handled = true;
1080 Invoked?.Invoke(this);
1081 }
1082
1084 protected override void OnDragged(WidgetDragEventArgs args)
1085 {
1086 base.OnDragged(args);
1087 if (_dragView is not null)
1088 ReorderDragged?.Invoke(this, args, _dragView);
1089 }
1090
1092 protected override void OnDragEnded(WidgetDragEventArgs args)
1093 {
1094 base.OnDragEnded(args);
1095 ReorderDragEnded?.Invoke(this);
1096 _dragView = null;
1097 }
1098
1100 public override void Dispose()
1101 {
1102 if (_disposed)
1103 return;
1104
1105 _disposed = true;
1106 base.Dispose();
1107 Invoked = null;
1108 ReorderDragged = null;
1109 ReorderDragEnded = null;
1110 }
1111
1112 private void CompleteInitialization()
1113 {
1114 Add(Background);
1115 Add(Label);
1116
1117 SetHeaderZOrder(0);
1118 CanReceiveFocus = true;
1120 IsDragEnabled = false;
1121 }
1122
1123 private void UpdateBackgroundColor(Color color)
1124 {
1125 Background.SetColor(color);
1126 Background.SetPosition(Background.GetPosition());
1127 }
1128
1129 private static DirectRectangle CreateBackground(RenderSurfaceHostBase renderSurfaceHost,
1130 View view,
1131 Rectangle bounds,
1132 string nickname)
1133 {
1134 return new DirectRectangle(Color.FromArgb(255, 60, 60, 68),
1135 renderSurfaceHost,
1136 view,
1137 bounds,
1138 $"{nickname}.background")
1139 .SetFilled(true)
1140 .SetBorderColor(Color.FromArgb(255, 150, 150, 160))
1141 .SetStrokeWidth(1.5f)
1142 .SetCornerRadius(5f);
1143 }
1144
1145 private static DirectRectangle CreateBackground(RenderSurfaceHostBase renderSurfaceHost,
1146 SceneLayer sceneLayer,
1147 Rectangle bounds,
1148 string nickname)
1149 {
1150 return new DirectRectangle(Color.FromArgb(255, 60, 60, 68),
1151 renderSurfaceHost,
1152 sceneLayer,
1153 bounds,
1154 $"{nickname}.background")
1155 .SetFilled(true)
1156 .SetBorderColor(Color.FromArgb(255, 150, 150, 160))
1157 .SetStrokeWidth(1.5f)
1158 .SetCornerRadius(5f);
1159 }
1160
1161 private static TextBlock CreateLabel(RenderSurfaceHostBase renderSurfaceHost,
1162 View view,
1163 Rectangle bounds,
1164 string text)
1165 {
1166 return new TextBlock(renderSurfaceHost, view, bounds)
1167 .SetText(text)
1168 .SetFont(SKTypeface.Default, 16f, minSize: 10f)
1169 .SetColors(SKColors.White, SKColors.Transparent)
1170 .SetAlignment(SKTextAlign.Center, TextBlock.VerticalAlign.Center)
1171 .EnableWrapping(false);
1172 }
1173
1174 private static TextBlock CreateLabel(RenderSurfaceHostBase renderSurfaceHost,
1175 SceneLayer sceneLayer,
1176 Rectangle bounds,
1177 string text)
1178 {
1179 return new TextBlock(renderSurfaceHost, sceneLayer, view: null, worldBounds: bounds)
1180 .SetText(text)
1181 .SetFont(SKTypeface.Default, 16f, minSize: 10f)
1182 .SetColors(SKColors.White, SKColors.Transparent)
1183 .SetAlignment(SKTextAlign.Center, TextBlock.VerticalAlign.Center)
1184 .EnableWrapping(false);
1185 }
1186 }
1187}
RenderSurfaceHostBase RenderSurfaceHost
Gets the render surface host shared by the composite and its children.
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
DirectComposite SetLocalOffset(IDirectCompositeChild child, Vector2 newLocalOffsetPx)
Changes one child's local offset and reapplies its position.
View? View
Gets the view shared by all children in view mode.
Vector2 GetPosition()
Gets the current composite anchor.
void SetPosition(Vector2 position)
Sets the composite anchor and repositions all children from their stored offsets.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
override DirectComposite Remove(IDirectCompositeChild child)
Removes a child without disposing it.The current composite.
override DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.The current composite.
ContainerWidget(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the ContainerWidget class.
TabControlWidget Relayout()
Recomputes header and page layout after changing a size or tab-layout setting.
override void Dispose()
Disposes the composite and every child it owns.
TabControlWidget SelectTab(int index)
Selects the page at the supplied tab index.
bool AutoExpandRows
Gets or sets whether the control adds rows as more tabs are added.
override void OnKeyboardInput(WidgetKeyboardEventArgs args)
Called when keyboard input is routed to the widget.
TabPageWidget AddTab(string title, char? mnemonic=null, string? nickname=null)
Adds a page to the end of the tab strip.
TabControlWidget SetTabControlZOrder(int zOrder)
Sets the base Z-order for all page and header visuals.
int TabRowCount
Gets or sets the number of header rows reserved when AutoExpandRows is disabled.
TabPageWidget? SelectedTab
Gets the selected page, or null when the control has no pages.
TabControlWidget SetTabColors(Color normal, Color hover, Color pressed, Color selected)
Sets the tab header colors.
bool IsTabReorderingEnabled
Gets or sets whether headers can be drag-reordered with a primary pointer.
bool RemoveTab(TabPageWidget page, bool dispose=false)
Removes a page from the tab control.
int PreferredTabWidth
Gets or sets the preferred header width used when rows expand automatically.
Action< TabPageWidget, int, int >? TabReordered
Occurs after a page moves to a different tab index.
Rectangle Bounds
Gets the current control bounds in its native coordinate space.
override void ProcessShown()
Performs processing when the widget is shown before notification callbacks are invoked.
int SelectedIndex
Gets the selected tab index, or -1 when the control has no pages.
override void ProcessHidden()
Performs processing when the widget is hidden before notification callbacks are invoked.
TabControlWidget MoveTab(int fromIndex, int toIndex)
Moves one page to a different tab index.
TabControlWidget SetTabTextColor(Color color)
Sets the color used by tab-header labels.
IReadOnlyList< TabPageWidget > Tabs
Gets the pages in their visible tab order.
int DisplayedTabRowCount
Gets the number of header rows currently occupied or reserved by the control.
TabControlWidget SelectTab(TabPageWidget page)
Selects the supplied page.
int DisplayedTabColumnCount
Gets the number of tab cells displayed across each header row.
TabControlWidget MoveTab(TabPageWidget page, int toIndex)
Moves the supplied page to a different tab index.
TabControlWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle bounds, string? nickname=null)
Creates a view-level tab control.
float TabReorderDragThresholdPx
Gets or sets the pointer movement required to begin a tab reordering drag.
int TabHeight
Gets or sets the height of one tab header row in native pixels.
TabControlWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle bounds, string? nickname=null)
Creates a scene-layer tab control.
Action< TabPageWidget >? SelectedTabChanged
Occurs after the selected page changes.
override void ProcessActivated()
Performs processing when the widget is activated before notification callbacks are invoked.
Size Size
Gets or sets the overall control size while preserving its anchor position.
Represents one selectable page owned by a TabControlWidget.
bool IsDragEnabled
Gets or sets whether new drag operations may begin.
bool CanReceiveFocus
Gets or sets a value indicating whether the widget can receive keyboard focus.
bool IsPointerInputEnabled
Gets or sets a value indicating whether the widget can receive pointer input.
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.
@ 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