Files
RobustToolbox/Robust.Client/UserInterface/Controls/TabContainer.cs
eoineoineoin e714dcc83c Fix TabContainer click detection when UIScale was not == 1.0 (#5456)
* Fix tabcontainer click detection when UIScale was not == 1.0

* Remove whitespace

---------

Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-09-22 14:41:27 +02:00

392 lines
12 KiB
C#

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<bool> TabVisibleProperty = AttachedProperty<bool>.Create("TabVisible", typeof(TabContainer), true);
public static readonly AttachedProperty<string?> TabTitleProperty = AttachedProperty<string?>.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<float> _tabRight = new();
public int CurrentTab
{
get => _currentTab;
set
{
if (_currentTab < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Current tab must be positive.");
}
if (_currentTab >= 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<int>? 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<StyleBox>(StylePropertyTabStyleBox, out var box);
return box;
}
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getTabBoxInactive()
{
TryGetStyleProperty<StyleBox>(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<StyleBox>(StylePropertyPanelStyleBox, out var box);
return box;
}
[System.Diagnostics.Contracts.Pure]
private Font _getFont()
{
if (TryGetStyleProperty<Font>("font", out var font))
{
return font;
}
return UserInterfaceManager.ThemeDefaults.DefaultFont;
}
}
}