New GridContainer capabilities and customizable tooltips (#1395)

* #272 avoid mouse overlapping tooltip when near edges,
change tooltip colors to match mockups

* #272 WIP customizable tooltips, old approach currently working still

* #272 WIP customizable tooltips, old approach currently working still

* #272 ensure tooltips go away when disposing control

* #272 implement row-oriented GridContainer

* #272 generalize GridContainer to support
rows or cols

* #272 improve readability in new GridContainer
logic

* #272 GridContainer can expand in opposite
direction

* #272 GridContainer can expand in opposite
direction

* #272 GridContainer can expand in opposite
direction, fix test

* #272 add GridContainer capability to
limit by size rather than count

* #272 add some clarifications about ui scale and vp / rp

* #272 don't spam showtooltip
event, calculate tooltip
positioning using combined
minimum size
This commit is contained in:
chairbender
2020-11-09 20:21:32 -08:00
committed by GitHub
parent 791fcfd65e
commit ee381804ec
7 changed files with 732 additions and 108 deletions

View File

@@ -34,7 +34,7 @@ namespace Robust.Client.Interfaces.UserInterface
Control? CurrentlyHovered { get; }
float UIScale { get; }
/// <summary>
/// Gets the default UIScale that we will use if <see cref="CVars.DisplayUIScale"/> gets set to 0.
/// Based on the OS-assigned window scale factor.
@@ -134,6 +134,10 @@ namespace Robust.Client.Interfaces.UserInterface
void QueueStyleUpdate(Control control);
void QueueLayoutUpdate(Control control);
void CursorChanged(Control control);
/// <summary>
/// Hides the tooltip for the indicated control, if tooltip for that control is currently showing.
/// </summary>
void HideTooltipFor(Control control);
}
}

View File

@@ -30,6 +30,9 @@ namespace Robust.Client.UserInterface
/// <summary>
/// The amount of "real" pixels a virtual pixel takes up.
/// The higher the number, the bigger the interface.
/// I.e. UIScale units are real pixels (rp) / virtual pixels (vp),
/// real pixels varies depending on interface, virtual pixels doesn't.
/// And vp * UIScale = rp, and rp / UIScale = vp
/// </summary>
[ViewVariables]
protected float UIScale => UserInterfaceManager.UIScale;

View File

@@ -192,13 +192,42 @@ namespace Robust.Client.UserInterface
/// <summary>
/// The tooltip that is shown when the mouse is hovered over this control for a bit.
/// Simple text tooltip that is shown when the mouse is hovered over this control for a bit.
/// See <see cref="OnShowTooltip"/> for a more customizable alternative.
/// </summary>
/// <remarks>
/// If empty or null, no tooltip is shown in the first place.
/// If empty or null, no tooltip is shown in the first place (but OnShowTooltip and OnHideTooltip
/// events are still fired).
/// </remarks>
public string? ToolTip { get; set; }
/// <summary>
/// Invoked when the mouse is hovered over this control for a bit and a tooltip
/// should be shown. Can be used as an alternative to ToolTip to perform custom tooltip
/// logic such as showing a more complex tooltip control.
///
/// Any custom tooltip controls should typically be added
/// as a child of UserInterfaceManager.PopupRoot
/// Handlers can use <see cref="Tooltips.PositionTooltip(Control)"/> to assist with positioning
/// custom tooltip controls.
/// </summary>
public event EventHandler? OnShowTooltip;
internal void PerformShowTooltip()
{
OnShowTooltip?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invoked when this control is showing a tooltip which should now be hidden.
/// </summary>
public event EventHandler? OnHideTooltip;
internal void PerformHideTooltip()
{
OnHideTooltip?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// The mode that controls how mouse filtering works. See the enum for how it functions.
/// </summary>
@@ -373,6 +402,8 @@ namespace Robust.Client.UserInterface
return;
}
UserInterfaceManagerInternal.HideTooltipFor(this);
DisposeAllChildren();
Parent?.RemoveChild(this);

View File

@@ -1,59 +1,132 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// A container that lays out its children in a grid.
/// A container that lays out its children in a grid. Can define specific count of
/// rows or specific count of columns (not both), and will grow to fill in additional rows/columns within
/// that limit. Alternatively, can define a maximum width or height, and grid will
/// lay out elements (aligned in a grid pattern, not floated) within the defined limit.
/// </summary>
public class GridContainer : Container
{
private int _columns = 1;
// limit - depending on mode, this is either rows or columns
private int _limitedDimensionCount = 1;
// virtual pixels
private float _limitSize;
private LimitType _limitType = LimitType.Count;
private Dimension _limitDimension = Dimension.Column;
/// <summary>
/// The amount of columns to organize the children into.
/// Indicates whether row or column count has been specified, and thus
/// how items will fill them out as they are added.
/// This is set depending on whether you have specified Columns or Rows.
/// </summary>
public Dimension LimitedDimension => _limitDimension;
/// <summary>
/// Opposite dimension of LimitedDimension
/// </summary>
public Dimension UnlimitedDimension => _limitDimension == Dimension.Column ? Dimension.Row : Dimension.Column;
/// <summary>
/// Indicates whether we are limiting based on an explicit number of rows or columns, or limiting
/// based on a defined max width or height.
/// </summary>
public LimitType LimitType => _limitType;
/// <summary>
/// The "normal" direction of expansion when the defined row or column limit is met
/// is right (for row-limited) and down (for column-limited),
/// this inverts that so the container expands in the opposite direction as elements are added.
/// </summary>
public bool ExpandBackwards
{
get => _expandBackwards;
set
{
_expandBackwards = value;
UpdateLayout();
}
}
private bool _expandBackwards;
/// <summary>
/// The number of columns to organize the children into. Setting this puts this grid
/// into LimitMode.LimitColumns and LimitType.Count - items will be added to fill up the entire row, up to the defined
/// limit of columns, and then create a second row.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
/// <returns>specified limit if LimitMode.LimitColums, otherwise the number
/// of columns being used for the current amount of children.</returns>
public int Columns
{
get => _columns;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
}
_columns = value;
MinimumSizeChanged();
UpdateLayout();
}
get => GetCount(Dimension.Column);
set => SetCount(Dimension.Column, value);
}
/// <summary>
/// The amount of rows being used for the current amount of children.
/// The number of rows to organize the children into. Setting this puts this grid
/// into LimitMode.LimitRows and LimitType.Count - items will be added to fill up the entire column, up to the defined
/// limit of rows, and then create a second column.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
/// <returns>specified limit if LimitMode.LimitRows, otherwise the number
/// of rows being used for the current number of children.</returns>
public int Rows
{
get
{
if (ChildCount == 0)
{
return 1;
}
var div = ChildCount / Columns;
if (ChildCount % Columns != 0)
{
div += 1;
}
return div;
}
get => GetCount(Dimension.Row);
set => SetCount(Dimension.Row, value);
}
/// <summary>
/// The max width (in virtual pixels) the grid of elements can have. This dynamically determines
/// the number of columns based on the size of the elements. Setting this puts this grid
/// into LimitMode.LimitColumns and LimitType.Size. Items will be added to fill up the entire row, up to the defined
/// width, and then create a second row.
///
/// In the presence of unevenly-sized children,
/// rows will still have the same amount elements - the items are laid out in a grid pattern such
/// that they are all aligned, the height and width of each "cell" being determined by
/// the greatest min height and min width among the elements. In the presence of expanding elements,
/// their pre-expanded size will be used to determine the cell layout, then the elements expand within
/// the defined Control.Size
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxWidth
{
set => SetMaxSize(Dimension.Column, value);
}
/// <summary>
/// The max height (in virtual pixels) the grid of elements can have. This dynamically determines
/// the number of rows based on the size of the elements. Setting this puts this grid
/// into LimitMode.LimitRows and LimitType.Size - items will be added to fill up the entire column, up to the defined
/// height, and then create a second column.
///
/// In the presence of unevenly-sized children,
/// columns will still have the same amount elements - the items are laid out in a grid pattern such
/// that they are all aligned, the height and width of each "cell" being determined by
/// the greatest min height and min width among the elements. In the presence of expanding elements,
/// their pre-expanded size will be used to determine the layout, then the elements expand within
/// the defined Control.Size
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxHeight
{
set => SetMaxSize(Dimension.Row, value);
}
private int? _vSeparationOverride;
@@ -75,15 +148,146 @@ namespace Robust.Client.UserInterface.Controls
private Vector2i Separations => (_hSeparationOverride ?? 4, _vSeparationOverride ?? 4);
private float GetLimitPixelSize()
{
return _limitSize * UIScale;
}
private int GetCount(Dimension forDimension)
{
if (_limitType == LimitType.Count)
{
if (forDimension == _limitDimension) return _limitedDimensionCount;
if (ChildCount == 0)
{
return 1;
}
var divisor = (_limitDimension == Dimension.Column ? Columns : Rows);
var div = ChildCount / divisor;
if (ChildCount % divisor != 0)
{
div += 1;
}
return div;
}
else
{
if (forDimension == _limitDimension) return CalculateLimitedCount();
if (ChildCount == 0)
{
return 1;
}
var divisor = CalculateLimitedCount();;
var div = ChildCount / divisor;
if (ChildCount % divisor != 0)
{
div += 1;
}
return div;
}
}
private void SetCount(Dimension forDimension, int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
}
_limitDimension = forDimension;
_limitType = LimitType.Count;
_limitedDimensionCount = value;
MinimumSizeChanged();
UpdateLayout();
}
private void SetMaxSize(Dimension forDimension, float value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
}
_limitDimension = forDimension;
_limitType = LimitType.Size;
_limitSize = value;
MinimumSizeChanged();
UpdateLayout();
}
/// <summary>
/// If columns (or width) are being limited, calculates how many columns
/// there should be.
/// </summary>
private int CalculateLimitedCount()
{
if (_limitType == LimitType.Count) return _limitedDimensionCount;
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns (or width) have a limit defined, thus
// the amount of rows is not limited (unlimited).
// we convert all elements to "cells" of the same size so they will align,
// also converting to our pretend scenario of limited columns/width
var (cellWidthActual, cellHeightActual) = CellSize();
var (wSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var cellWidth = _limitDimension == Dimension.Column ? cellWidthActual : cellHeightActual;
var wSep = _limitDimension == Dimension.Column ? wSepActual : hSepActual;
// calculate how many cells will fit into a given column without going over, accounting
// for additional wSep between each cell only if there's more than one
if (ChildCount == 0) return 1;
if ((2 * cellWidth + wSep) > GetLimitPixelSize())
{
return 1;
}
return Math.Min(ChildCount, (int) ((GetLimitPixelSize() + wSep) / (cellWidth + wSep)));
}
/// <summary>
/// Calculates the size of a "cell" in physical pixels, for use in LimitType.Size mode. This
/// is based on the maximum minheight / minwidth of each element.
/// </summary>
/// <returns></returns>
private Vector2i CellSize()
{
int maxMinWidth = -1;
int maxMinHeight = -1;
foreach (var child in Children)
{
var (minSizeX, minSizeY) = child.CombinedPixelMinimumSize;
maxMinWidth = Math.Max(maxMinWidth, minSizeX);
maxMinHeight = Math.Max(maxMinHeight, minSizeY);
}
return new Vector2i(maxMinWidth, maxMinHeight);
}
protected override Vector2 CalculateMinimumSize()
{
var (wSep, hSep) = (Vector2i) (Separations * UIScale);
var rows = Rows;
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns have a limit defined, thus
// the amount of rows is not limited (unlimited).
// Minimum width of the columns.
Span<int> columnSizes = stackalloc int[_columns];
// Minimum height of the rows.
Span<int> rowSizes = stackalloc int[rows];
var rows = GetCount(UnlimitedDimension);
var cols = GetCount(LimitedDimension);
var cellSize = CellSize();
Span<int> minColWidth = stackalloc int[cols];
Span<int> minRowHeight = stackalloc int[rows];
var index = 0;
foreach (var child in Children)
@@ -94,34 +298,45 @@ namespace Robust.Client.UserInterface.Controls
continue;
}
var row = index / _columns;
var column = index % _columns;
var column = index % cols;
var row = index / cols;
var (minSizeX, minSizeY) = child.CombinedPixelMinimumSize;
columnSizes[column] = Math.Max(minSizeX, columnSizes[column]);
rowSizes[row] = Math.Max(minSizeY, rowSizes[row]);
// also converting here to our "pretend" scenario where columns have a limit defined.
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.CombinedPixelMinimumSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
minRowHeight[row] = Math.Max(minSizeY, minRowHeight[row]);
index += 1;
}
var minWidth = AccumSizes(columnSizes, wSep);
var minHeight = AccumSizes(rowSizes, hSep);
// converting here to our "pretend" scenario where columns have a limit defined
var (wSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var wSep = _limitDimension == Dimension.Column ? wSepActual : hSepActual;
var hSep = _limitDimension == Dimension.Column ? hSepActual : wSepActual;
var minWidth = AccumSizes(minColWidth, wSep);
var minHeight = AccumSizes(minRowHeight, hSep);
return new Vector2(minWidth, minHeight) / UIScale;
// converting back from our pretend scenario where columns are limited
return new Vector2(
_limitDimension == Dimension.Column ? minWidth : minHeight,
_limitDimension == Dimension.Column ? minHeight : minWidth) / UIScale;
}
private static int AccumSizes(Span<int> sizes, int separator)
{
var totalSize = 0;
var firstColumn = true;
var first = true;
foreach (var size in sizes)
{
totalSize += size;
if (firstColumn)
if (first)
{
firstColumn = false;
first = false;
}
else
{
@@ -134,15 +349,24 @@ namespace Robust.Client.UserInterface.Controls
protected override void LayoutUpdateOverride()
{
var rows = Rows;
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns have a limit defined, thus
// the amount of rows is not limited (unlimited).
// Minimum width of the columns.
Span<int> columnSizes = stackalloc int[_columns];
// Minimum height of the rows.
Span<int> rowSizes = stackalloc int[rows];
// Columns that are set to expand horizontally.
Span<bool> columnExpand = stackalloc bool[_columns];
// Columns that are set to expand vertically.
var rows = GetCount(UnlimitedDimension);
var cols = GetCount(LimitedDimension);
var cellSize = CellSize();
Span<int> minColWidth = stackalloc int[cols];
// Minimum lateral size of the unlimited dimension
// (i.e. width of columns, height of rows).
Span<int> minRowHeight = stackalloc int[rows];
// columns that are set to expand vertically
Span<bool> colExpand = stackalloc bool[cols];
// rows that are set to expand horizontally
Span<bool> rowExpand = stackalloc bool[rows];
// Get minSize and size flag expand of each column and row.
@@ -155,20 +379,29 @@ namespace Robust.Client.UserInterface.Controls
continue;
}
var row = index / _columns;
var column = index % _columns;
var column = index % cols;
var row = index / cols;
var (minSizeX, minSizeY) = child.CombinedPixelMinimumSize;
columnSizes[column] = Math.Max(minSizeX, columnSizes[column]);
rowSizes[row] = Math.Max(minSizeY, rowSizes[row]);
columnExpand[column] = columnExpand[column] || (child.SizeFlagsHorizontal & SizeFlags.Expand) != 0;
rowExpand[row] = rowExpand[row] || (child.SizeFlagsVertical & SizeFlags.Expand) != 0;
// converting here to our "pretend" scenario where columns have a limit defined
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.CombinedPixelMinimumSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
minRowHeight[row] = Math.Max(minSizeY, minRowHeight[row]);
var colSizeFlag = _limitDimension == Dimension.Column
? child.SizeFlagsHorizontal
: child.SizeFlagsVertical;
var rowSizeFlag = UnlimitedDimension == Dimension.Column
? child.SizeFlagsHorizontal
: child.SizeFlagsVertical;
colExpand[column] = colExpand[column] || (colSizeFlag & SizeFlags.Expand) != 0;
rowExpand[row] = rowExpand[row] || (rowSizeFlag & SizeFlags.Expand) != 0;
index += 1;
}
// Basically now we just apply BoxContainer logic on rows and columns.
var (vSep, hSep) = (Vector2i) (Separations * UIScale);
var stretchMinX = 0;
var stretchMinY = 0;
// We do not use stretch ratios because Godot doesn't,
@@ -179,11 +412,11 @@ namespace Robust.Client.UserInterface.Controls
var stretchCountX = 0;
var stretchCountY = 0;
for (var i = 0; i < columnSizes.Length; i++)
for (var i = 0; i < minColWidth.Length; i++)
{
if (!columnExpand[i])
if (!colExpand[i])
{
stretchMinX += columnSizes[i];
stretchMinX += minColWidth[i];
}
else
{
@@ -191,11 +424,11 @@ namespace Robust.Client.UserInterface.Controls
}
}
for (var i = 0; i < rowSizes.Length; i++)
for (var i = 0; i < minRowHeight.Length; i++)
{
if (!rowExpand[i])
{
stretchMinY += rowSizes[i];
stretchMinY += minRowHeight[i];
}
else
{
@@ -203,35 +436,74 @@ namespace Robust.Client.UserInterface.Controls
}
}
var stretchMaxX = Width - hSep * (_columns - 1);
var stretchMaxY = Height - vSep * (rows - 1);
// converting here to our "pretend" scenario where columns have a limit defined
var (vSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var hSep = _limitDimension == Dimension.Column ? hSepActual : vSepActual;
var vSep = _limitDimension == Dimension.Column ? vSepActual : hSepActual;
var width = _limitDimension == Dimension.Column ? Width : Height;
var height = _limitDimension == Dimension.Column ? Height : Width;
var stretchMaxX = width - hSep * (cols - 1);
var stretchMaxY = height - vSep * (rows - 1);
var stretchAvailX = Math.Max(0, stretchMaxX - stretchMinX);
var stretchAvailY = Math.Max(0, stretchMaxY - stretchMinY);
for (var i = 0; i < columnSizes.Length; i++)
for (var i = 0; i < minColWidth.Length; i++)
{
if (!columnExpand[i])
if (!colExpand[i])
{
continue;
}
columnSizes[i] = (int) (stretchAvailX / stretchCountX);
minColWidth[i] = (int) (stretchAvailX / stretchCountX);
}
for (var i = 0; i < rowSizes.Length; i++)
for (var i = 0; i < minRowHeight.Length; i++)
{
if (!rowExpand[i])
{
continue;
}
rowSizes[i] = (int) (stretchAvailY / stretchCountY);
minRowHeight[i] = (int) (stretchAvailY / stretchCountY);
}
// Actually lay them out.
var vOffset = 0;
// if inverted, (in our pretend "columns are limited" scenario) we must calculate the final
// height (as height will vary depending on number of elements), and then
// go backwards, starting from the bottom and filling elements in upwards
var finalVOffset = 0;
if (ExpandBackwards)
{
// we have to iterate through the elements first to determine the height each
// row will end up having, as they can vary
index = 0;
for (var i = 0; i < ChildCount; i++, index++)
{
var child = GetChild(i);
if (!child.Visible)
{
index--;
continue;
}
var column = index % cols;
var row = index / cols;
if (column == 0)
{
// Just started a new row/col.
if (row != 0)
{
finalVOffset += vSep + minRowHeight[row - 1];
}
}
}
}
var hOffset = 0;
var vOffset = ExpandBackwards ? finalVOffset : 0;
index = 0;
for (var i = 0; i < ChildCount; i++, index++)
{
@@ -242,24 +514,59 @@ namespace Robust.Client.UserInterface.Controls
continue;
}
var row = index / _columns;
var column = index % _columns;
var column = index % cols;
var row = index / cols;
if (column == 0)
{
// Just started a new row.
// Just started a new row
hOffset = 0;
if (row != 0)
{
vOffset += vSep + rowSizes[row - 1];
if (ExpandBackwards)
{
// every time we start a new row we actually decrease the voffset, we are filling
// in the up direction
vOffset -= vSep + minRowHeight[row - 1];
}
else
{
vOffset += vSep + minRowHeight[row - 1];
}
}
}
var box = UIBox2i.FromDimensions(hOffset, vOffset, columnSizes[column], rowSizes[row]);
// converting back from our "pretend" scenario
var left = _limitDimension == Dimension.Column ? hOffset : vOffset;
var top = _limitDimension == Dimension.Column ? vOffset : hOffset;
var boxWidth = _limitDimension == Dimension.Column ? minColWidth[column] : minRowHeight[row];
var boxHeight = _limitDimension == Dimension.Column ? minRowHeight[row] : minColWidth[column];
var box = UIBox2i.FromDimensions(left, top, boxWidth, boxHeight);
FitChildInPixelBox(child, box);
hOffset += columnSizes[column] + hSep;
hOffset += minColWidth[column] + hSep;
}
}
}
public enum Dimension
{
Column,
Row
}
public enum LimitType
{
/// <summary>
/// Defined number of rows or columns
/// </summary>
Count,
/// <summary>
/// Defined max width or height, inside of which the number of rows or columns
/// will be fit.
/// </summary>
Size
}
}

View File

@@ -0,0 +1,56 @@
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface
{
/// <summary>
/// Utilities for working with tooltips.
/// </summary>
public static class Tooltips
{
/// <summary>
/// Positions the provided control as a tooltip within the bounds of its parent UserInterfaceManager screen
/// under the current mouse position. Sizing Based on its current combined minimum size.
/// Defaults to the top left corner
/// of the control being placed at the mouse position but
/// adjusting to a different corner if the control would go beyond the edge of the bounds.
/// </summary>
/// <param name="tooltip">control to position (current size will be used to determine bounds)</param>
public static void PositionTooltip(Control tooltip)
{
PositionTooltip(tooltip.UserInterfaceManager.RootControl.Size,
tooltip.UserInterfaceManager.MousePositionScaled,
tooltip);
}
/// <summary>
/// Positions the provided control as a tooltip within the provided screenBounds based on its current
/// combined minimum size.
/// Defaults to the top left corner
/// of the control being placed at the indicated position but
/// adjusting to a different corner if the control would go beyond the edge of the bounds.
/// </summary>
/// <param name="screenBounds">max x and y screen coordinates for the tooltip to occupy, tooltip
/// will be shifted to avoid exceeding these bounds.</param>
/// <param name="screenPosition">position to place the tooltip at, in screen coordinates</param>
/// <param name="tooltip">control to position (current size will be used to determine bounds)</param>
public static void PositionTooltip(Vector2 screenBounds, Vector2 screenPosition, Control tooltip)
{
LayoutContainer.SetPosition(tooltip, screenPosition);
var combinedMinSize = tooltip.CombinedMinimumSize;
var (right, bottom) = tooltip.Position + combinedMinSize;
if (right > screenBounds.X)
{
LayoutContainer.SetPosition(tooltip, (screenPosition.X - combinedMinSize.X, tooltip.Position.Y));
}
if (bottom > screenBounds.Y)
{
LayoutContainer.SetPosition(tooltip, (tooltip.Position.X, screenPosition.Y - combinedMinSize.Y));
}
}
}
}

View File

@@ -78,6 +78,7 @@ namespace Robust.Client.UserInterface
private bool _rendering = true;
private float _tooltipTimer;
private Tooltip _tooltip = default!;
private bool showingTooltip;
private const float TooltipDelay = 1;
private readonly Queue<Control> _styleUpdateQueue = new Queue<Control>();
@@ -692,8 +693,20 @@ namespace Robust.Client.UserInterface
private void _clearTooltip()
{
if (!showingTooltip) return;
_tooltip.Visible = false;
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
showingTooltip = false;
}
public void HideTooltipFor(Control control)
{
if (CurrentlyHovered == control)
{
_clearTooltip();
}
}
private void _resetTooltipTimer()
@@ -703,27 +716,23 @@ namespace Robust.Client.UserInterface
private void _showTooltip()
{
if (showingTooltip) return;
showingTooltip = true;
var hovered = CurrentlyHovered;
if (hovered == null || string.IsNullOrWhiteSpace(hovered.ToolTip))
if (hovered == null)
{
return;
}
_tooltip.Visible = true;
_tooltip.Text = hovered.ToolTip;
LayoutContainer.SetPosition(_tooltip, MousePositionScaled);
var (right, bottom) = _tooltip.Position + _tooltip.Size;
if (right > RootControl.Size.X)
// show simple tooltip if there is one
if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
{
LayoutContainer.SetPosition(_tooltip, (RootControl.Size.X - _tooltip.Size.X, _tooltip.Position.Y));
_tooltip.Visible = true;
_tooltip.Text = hovered.ToolTip;
Tooltips.PositionTooltip(_tooltip);
}
if (bottom > RootControl.Size.Y)
{
LayoutContainer.SetPosition(_tooltip, (_tooltip.Position.X, RootControl.Size.Y - _tooltip.Size.Y));
}
hovered.PerformShowTooltip();
}
private void _uiScaleChanged(float newValue)

View File

@@ -16,10 +16,11 @@ namespace Robust.UnitTesting.Client.UserInterface.Controls
{
public override UnitTestProject Project => UnitTestProject.Client;
[Test]
public void TestBasic()
[TestCase(true)]
[TestCase(false)]
public void TestBasic(bool limitByCount)
{
var grid = new GridContainer {Columns = 2};
var grid = limitByCount ? new GridContainer {Columns = 2} : new GridContainer { MaxWidth = 125};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
@@ -43,10 +44,160 @@ namespace Robust.UnitTesting.Client.UserInterface.Controls
Assert.That(child5.Position, Is.EqualTo(new Vector2(0, 108)));
}
[Test]
public void TestExpand()
[TestCase(true)]
[TestCase(false)]
public void TestBasicRows(bool limitByCount)
{
var grid = new GridContainer {Columns = 2, Size = (200, 200)};
var grid = limitByCount ? new GridContainer {Rows = 2}
: new GridContainer {MaxHeight = 125};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
var child4 = new Control {CustomMinimumSize = (50, 50)};
var child5 = new Control {CustomMinimumSize = (50, 50)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(grid.CombinedMinimumSize, Is.EqualTo(new Vector2(158, 104)));
Assert.That(child1.Position, Is.EqualTo(Vector2.Zero));
Assert.That(child2.Position, Is.EqualTo(new Vector2(0, 54)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(54, 0)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 54)));
Assert.That(child5.Position, Is.EqualTo(new Vector2(108, 0)));
}
[Test]
public void TestUnevenLimitSize()
{
// when uneven sizes are used and limiting by size, they should all be treated as equal size cells based on the
// max minwidth / minheight among them.
// Note that when limiting by count, the behavior is different - rows and columns are individually
// expanded based on the max size of their elements
var grid = new GridContainer { MaxWidth = 125};
var child1 = new Control {CustomMinimumSize = (12, 24)};
var child2 = new Control {CustomMinimumSize = (30, 50)};
var child3 = new Control {CustomMinimumSize = (40, 20)};
var child4 = new Control {CustomMinimumSize = (20, 12)};
var child5 = new Control {CustomMinimumSize = (50, 10)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(grid.CombinedMinimumSize, Is.EqualTo(new Vector2(104, 158)));
Assert.That(child1.Position, Is.EqualTo(Vector2.Zero));
Assert.That(child2.Position, Is.EqualTo(new Vector2(54, 0)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(0, 54)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 54)));
Assert.That(child5.Position, Is.EqualTo(new Vector2(0, 108)));
}
[Test]
public void TestUnevenLimitSizeRows()
{
var grid = new GridContainer {MaxHeight = 125};
var child1 = new Control {CustomMinimumSize = (12, 2)};
var child2 = new Control {CustomMinimumSize = (5, 23)};
var child3 = new Control {CustomMinimumSize = (42, 4)};
var child4 = new Control {CustomMinimumSize = (2, 50)};
var child5 = new Control {CustomMinimumSize = (50, 34)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(grid.CombinedMinimumSize, Is.EqualTo(new Vector2(158, 104)));
Assert.That(child1.Position, Is.EqualTo(Vector2.Zero));
Assert.That(child2.Position, Is.EqualTo(new Vector2(0, 54)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(54, 0)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 54)));
Assert.That(child5.Position, Is.EqualTo(new Vector2(108, 0)));
}
[TestCase(true)]
[TestCase(false)]
public void TestBasicBackwards(bool limitByCount)
{
var grid = limitByCount ? new GridContainer {Columns = 2, ExpandBackwards = true}
: new GridContainer { MaxWidth = 125, ExpandBackwards = true};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
var child4 = new Control {CustomMinimumSize = (50, 50)};
var child5 = new Control {CustomMinimumSize = (50, 50)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(grid.CombinedMinimumSize, Is.EqualTo(new Vector2(104, 158)));
Assert.That(child1.Position, Is.EqualTo(new Vector2(0, 108)));
Assert.That(child2.Position, Is.EqualTo(new Vector2(54, 108)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(0, 54)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 54)));
Assert.That(child5.Position, Is.EqualTo(Vector2.Zero));
}
[TestCase(true)]
[TestCase(false)]
public void TestBasicRowsBackwards(bool limitByCount)
{
var grid = limitByCount ? new GridContainer {Rows = 2, ExpandBackwards = true}
: new GridContainer {MaxHeight = 125, ExpandBackwards = true};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
var child4 = new Control {CustomMinimumSize = (50, 50)};
var child5 = new Control {CustomMinimumSize = (50, 50)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(grid.CombinedMinimumSize, Is.EqualTo(new Vector2(158, 104)));
Assert.That(child1.Position, Is.EqualTo(new Vector2(108, 0)));
Assert.That(child2.Position, Is.EqualTo(new Vector2(108, 54)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(54, 0)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 54)));
Assert.That(child5.Position, Is.EqualTo(Vector2.Zero));
}
[TestCase(true)]
[TestCase(false)]
public void TestExpand(bool limitByCount)
{
// in the presence of a MaxWidth with expanding elements, the
// pre-expanded size should be used to determine the size of each "cell", and then expansion
// happens within the defined control size
var grid = limitByCount ? new GridContainer {Columns = 2, Size = (200, 200)}
: new GridContainer {MaxWidth = 125, Size = (200, 200)} ;
var child1 = new Control {CustomMinimumSize = (50, 50), SizeFlagsHorizontal = Control.SizeFlags.FillExpand};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
@@ -73,10 +224,44 @@ namespace Robust.UnitTesting.Client.UserInterface.Controls
Assert.That(child5.Size, Is.EqualTo(new Vector2(146, 50)));
}
[Test]
public void TestRowCount()
[TestCase(true)]
[TestCase(false)]
public void TestExpandRows(bool limitByCount)
{
var grid = new GridContainer {Columns = 2};
var grid = limitByCount ? new GridContainer {Rows = 2, Size = (200, 200)}
: new GridContainer {MaxHeight = 125, Size = (200, 200)};
var child1 = new Control {CustomMinimumSize = (50, 50), SizeFlagsVertical = Control.SizeFlags.FillExpand};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
var child4 = new Control {CustomMinimumSize = (50, 50), SizeFlagsHorizontal = Control.SizeFlags.FillExpand};
var child5 = new Control {CustomMinimumSize = (50, 50)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
grid.ForceRunLayoutUpdate();
Assert.That(child1.Position, Is.EqualTo(Vector2.Zero));
Assert.That(child1.Size, Is.EqualTo(new Vector2(50, 146)));
Assert.That(child2.Position, Is.EqualTo(new Vector2(0, 150)));
Assert.That(child2.Size, Is.EqualTo(new Vector2(50, 50)));
Assert.That(child3.Position, Is.EqualTo(new Vector2(54, 0)));
Assert.That(child3.Size, Is.EqualTo(new Vector2(92, 146)));
Assert.That(child4.Position, Is.EqualTo(new Vector2(54, 150)));
Assert.That(child4.Size, Is.EqualTo(new Vector2(92, 50)));
Assert.That(child5.Position, Is.EqualTo(new Vector2(150, 0)));
Assert.That(child5.Size, Is.EqualTo(new Vector2(50, 146)));
}
[TestCase(true)]
[TestCase(false)]
public void TestRowCount(bool limitByCount)
{
var grid = limitByCount ? new GridContainer {Columns = 2}
: new GridContainer {MaxWidth = 125};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
@@ -99,5 +284,34 @@ namespace Robust.UnitTesting.Client.UserInterface.Controls
Assert.That(grid.Rows, Is.EqualTo(1));
}
[TestCase(true)]
[TestCase(false)]
public void TestColumnCountRows(bool limitByCount)
{
var grid = limitByCount ? new GridContainer {Rows = 2}
: new GridContainer{MaxHeight = 125};
var child1 = new Control {CustomMinimumSize = (50, 50)};
var child2 = new Control {CustomMinimumSize = (50, 50)};
var child3 = new Control {CustomMinimumSize = (50, 50)};
var child4 = new Control {CustomMinimumSize = (50, 50)};
var child5 = new Control {CustomMinimumSize = (50, 50)};
grid.AddChild(child1);
grid.AddChild(child2);
grid.AddChild(child3);
grid.AddChild(child4);
grid.AddChild(child5);
Assert.That(grid.Columns, Is.EqualTo(3));
grid.RemoveChild(child5);
Assert.That(grid.Columns, Is.EqualTo(2));
grid.DisposeAllChildren();
Assert.That(grid.Columns, Is.EqualTo(1));
}
}
}