using System; using System.Collections.Generic; using System.Numerics; using Robust.Client.Graphics; using Robust.Shared.Input; using Robust.Shared.Localization; using Robust.Shared.Maths; namespace Robust.Client.UserInterface.Controls { [Virtual] public class TabContainer : Container { public static readonly AttachedProperty TabVisibleProperty = AttachedProperty.Create("TabVisible", typeof(TabContainer), true); public static readonly AttachedProperty TabTitleProperty = AttachedProperty.CreateNull("TabTitle", typeof(TabContainer)); public const string StylePropertyTabStyleBox = "tab-stylebox"; public const string StylePropertyTabStyleBoxInactive = "tab-stylebox-inactive"; public const string stylePropertyTabFontColor = "tab-font-color"; public const string StylePropertyTabFontColorInactive = "tab-font-color-inactive"; public const string StylePropertyPanelStyleBox = "panel-stylebox"; private int _currentTab; private bool _tabsVisible = true; // The right-most coordinate of each tab header private List _tabRight = new(); public int CurrentTab { get => _currentTab; set { if (value < 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must be positive."); } if (value >= ChildCount) { throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must less than the amount of tabs."); } if (_currentTab == value) { return; } var old = _currentTab; _currentTab = value; GetChild(old).Visible = false; var newSelected = GetChild(value); newSelected.Visible = true; InvalidateMeasure(); OnTabChanged?.Invoke(value); } } public bool TabsVisible { get => _tabsVisible; set { _tabsVisible = value; InvalidateMeasure(); } } public StyleBox? PanelStyleBoxOverride { get; set; } public Color? TabFontColorOverride { get; set; } public Color? TabFontColorInactiveOverride { get; set; } public event Action? OnTabChanged; public TabContainer() { MouseFilter = MouseFilterMode.Pass; } public string GetActualTabTitle(int tab) { var control = GetChild(tab); var title = control.GetValue(TabTitleProperty); return title ?? control.Name ?? Loc.GetString("tab-container-not-tab-title-provided"); } public static string? GetTabTitle(Control control) { return control.GetValue(TabTitleProperty); } public bool GetTabVisible(int tab) { var control = GetChild(tab); return GetTabVisible(control); } public static bool GetTabVisible(Control control) { return control.GetValue(TabVisibleProperty); } public void SetTabTitle(int tab, string title) { var control = GetChild(tab); SetTabTitle(control, title); } public static void SetTabTitle(Control control, string title) { control.SetValue(TabTitleProperty, title); } public void SetTabVisible(int tab, bool visible) { var control = GetChild(tab); SetTabVisible(control, visible); } public static void SetTabVisible(Control control, bool visible) { control.SetValue(TabVisibleProperty, visible); } protected override void ChildAdded(Control newChild) { base.ChildAdded(newChild); if (ChildCount == 1) { // This is our first child so it must always be visible. newChild.Visible = true; } else { // If not this can't be the currently selected tab so just make it invisible immediately. newChild.Visible = false; } } protected internal override void Draw(DrawingHandleScreen handle) { base.Draw(handle); // First, draw panel. var headerSize = _getHeaderSize(); var panel = _getPanel(); var panelBox = new UIBox2(0, headerSize, PixelWidth, PixelHeight); panel?.Draw(handle, panelBox, UIScale); var font = _getFont(); var boxActive = _getTabBoxActive(); var boxInactive = _getTabBoxInactive(); var fontColorActive = _getTabFontColorActive(); var fontColorInactive = _getTabFontColorInactive(); var headerOffset = 0f; _tabRight.Clear(); // Then, draw the tabs. for (var i = 0; i < ChildCount; i++) { if (!GetTabVisible(i)) { _tabRight.Add(headerOffset); continue; } var title = GetActualTabTitle(i); var titleLength = 0; // Get string length. foreach (var rune in title.EnumerateRunes()) { if (!font.TryGetCharMetrics(rune, UIScale, out var metrics)) { continue; } titleLength += metrics.Advance; } var active = _currentTab == i; var box = active ? boxActive : boxInactive; UIBox2 contentBox; var topLeft = new Vector2(headerOffset, 0); var size = new Vector2(titleLength, font.GetHeight(UIScale)); float boxAdvance; if (box != null) { var drawBox = box.GetEnvelopBox(topLeft, size, UIScale); boxAdvance = drawBox.Width; box.Draw(handle, drawBox, UIScale); contentBox = box.GetContentBox(drawBox, UIScale); } else { boxAdvance = size.X; contentBox = UIBox2.FromDimensions(topLeft, size); } var baseLine = new Vector2(0, font.GetAscent(UIScale)) + contentBox.TopLeft; foreach (var rune in title.EnumerateRunes()) { if (!font.TryGetCharMetrics(rune, UIScale, out var metrics)) { continue; } font.DrawChar(handle, rune, baseLine, UIScale, active ? fontColorActive : fontColorInactive); baseLine += new Vector2(metrics.Advance, 0); } headerOffset += boxAdvance; // Remember the right-most point of this tab, for testing clicked areas _tabRight.Add(headerOffset); } } protected override Vector2 MeasureOverride(Vector2 availableSize) { var headerSize = Vector2.Zero; if (TabsVisible) { headerSize = new Vector2(0, _getHeaderSize() / UIScale); } var panel = _getPanel(); var panelSize = (panel?.MinimumSize ?? Vector2.Zero); var contentsSize = availableSize - headerSize - panelSize; var total = Vector2.Zero; foreach (var child in Children) { if (child.Visible) { child.Measure(contentsSize); total = Vector2.Max(child.DesiredSize, total); } } return total + headerSize + panelSize; } protected override Vector2 ArrangeOverride(Vector2 finalSize) { if (ChildCount == 0 || _currentTab >= ChildCount) { return finalSize; } var headerSize = _getHeaderSize(); var panel = _getPanel(); var contentBox = new UIBox2i(0, headerSize, (int) (finalSize.X * UIScale), (int) (finalSize.Y * UIScale)); if (panel != null) { contentBox = (UIBox2i) panel.GetContentBox(contentBox, UIScale); } var control = GetChild(_currentTab); control.Visible = true; control.ArrangePixel(contentBox); return finalSize; } protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (!TabsVisible || args.Function != EngineKeyFunctions.UIClick) { return; } // Outside of header size, ignore. if (args.RelativePixelPosition.Y < 0 || args.RelativePixelPosition.Y > _getHeaderSize()) { return; } args.Handle(); var relX = args.RelativePixelPosition.X; float tabLeft = 0; for (var i = 0; i < ChildCount; i++) { if (relX > tabLeft && relX <= _tabRight[i]) { CurrentTab = i; return; } // Next tab starts here tabLeft = _tabRight[i]; } } // Returns the size of the header, in real pixels [System.Diagnostics.Contracts.Pure] private int _getHeaderSize() { var headerSize = 0; if (TabsVisible) { var active = _getTabBoxActive(); var inactive = _getTabBoxInactive(); var font = _getFont(); var activeSize = (active?.MinimumSize ?? Vector2.Zero) * UIScale; var inactiveSize = (inactive?.MinimumSize ?? Vector2.Zero) * UIScale; headerSize = (int) MathF.Max(activeSize.Y, inactiveSize.Y); headerSize += font.GetHeight(UIScale); } return headerSize; } [System.Diagnostics.Contracts.Pure] private StyleBox? _getTabBoxActive() { TryGetStyleProperty(StylePropertyTabStyleBox, out var box); return box; } [System.Diagnostics.Contracts.Pure] private StyleBox? _getTabBoxInactive() { TryGetStyleProperty(StylePropertyTabStyleBoxInactive, out var box); return box; } [System.Diagnostics.Contracts.Pure] private Color _getTabFontColorActive() { if (TabFontColorOverride != null) return TabFontColorOverride.Value; if (TryGetStyleProperty(stylePropertyTabFontColor, out Color color)) { return color; } return Color.White; } [System.Diagnostics.Contracts.Pure] private Color _getTabFontColorInactive() { if (TabFontColorInactiveOverride != null) return TabFontColorInactiveOverride.Value; if (TryGetStyleProperty(StylePropertyTabFontColorInactive, out Color color)) { return color; } return Color.Gray; } [System.Diagnostics.Contracts.Pure] private StyleBox? _getPanel() { if (PanelStyleBoxOverride != null) return PanelStyleBoxOverride; TryGetStyleProperty(StylePropertyPanelStyleBox, out var box); return box; } [System.Diagnostics.Contracts.Pure] private Font _getFont() { if (TryGetStyleProperty("font", out var font)) { return font; } return UserInterfaceManager.ThemeDefaults.DefaultFont; } } }