Guidebook Tables (#28484)
* PJB's cool table control (it probably doesn't work) * ok wait wrong file * Guidebook Tables
This commit is contained in:
@@ -11,6 +11,9 @@ public sealed class Box : BoxContainer, IDocumentTag
|
||||
HorizontalExpand = true;
|
||||
control = this;
|
||||
|
||||
if (args.TryGetValue("Margin", out var margin))
|
||||
Margin = new Thickness(float.Parse(margin));
|
||||
|
||||
if (args.TryGetValue("Orientation", out var orientation))
|
||||
Orientation = Enum.Parse<LayoutOrientation>(orientation);
|
||||
else
|
||||
|
||||
49
Content.Client/Guidebook/Richtext/ColorBox.cs
Normal file
49
Content.Client/Guidebook/Richtext/ColorBox.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Guidebook.Richtext;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ColorBox : PanelContainer, IDocumentTag
|
||||
{
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
VerticalExpand = true;
|
||||
control = this;
|
||||
|
||||
if (args.TryGetValue("Margin", out var margin))
|
||||
Margin = new Thickness(float.Parse(margin));
|
||||
|
||||
if (args.TryGetValue("HorizontalAlignment", out var halign))
|
||||
HorizontalAlignment = Enum.Parse<HAlignment>(halign);
|
||||
else
|
||||
HorizontalAlignment = HAlignment.Stretch;
|
||||
|
||||
if (args.TryGetValue("VerticalAlignment", out var valign))
|
||||
VerticalAlignment = Enum.Parse<VAlignment>(valign);
|
||||
else
|
||||
VerticalAlignment = VAlignment.Stretch;
|
||||
|
||||
var styleBox = new StyleBoxFlat();
|
||||
if (args.TryGetValue("Color", out var color))
|
||||
styleBox.BackgroundColor = Color.FromHex(color);
|
||||
|
||||
if (args.TryGetValue("OutlineThickness", out var outlineThickness))
|
||||
styleBox.BorderThickness = new Thickness(float.Parse(outlineThickness));
|
||||
else
|
||||
styleBox.BorderThickness = new Thickness(1);
|
||||
|
||||
if (args.TryGetValue("OutlineColor", out var outlineColor))
|
||||
styleBox.BorderColor = Color.FromHex(outlineColor);
|
||||
else
|
||||
styleBox.BorderColor = Color.White;
|
||||
|
||||
PanelOverride = styleBox;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
27
Content.Client/Guidebook/Richtext/Table.cs
Normal file
27
Content.Client/Guidebook/Richtext/Table.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Guidebook.Richtext;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class Table : TableContainer, IDocumentTag
|
||||
{
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
control = this;
|
||||
|
||||
if (!args.TryGetValue("Columns", out var columns) || !int.TryParse(columns, out var columnsCount))
|
||||
{
|
||||
Logger.Error("Guidebook tag \"Table\" does not specify required property \"Columns.\"");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Columns = columnsCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
285
Content.Client/UserInterface/Controls/TableContainer.cs
Normal file
285
Content.Client/UserInterface/Controls/TableContainer.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
// This control is not part of engine because I quickly wrote it in 2 hours at 2 AM and don't want to deal with
|
||||
// API stabilization and/or figuring out relation to GridContainer.
|
||||
// Grid layout is a complicated problem and I don't want to commit another half-baked thing into the engine.
|
||||
// It's probably sufficient for its use case (RichTextLabel tables for rules/guidebook).
|
||||
// Despite that, it's still better comment the shit half of you write on a regular basis.
|
||||
//
|
||||
// EMO: thank you PJB i was going to kill myself.
|
||||
|
||||
/// <summary>
|
||||
/// Displays children in a tabular grid. Unlike <see cref="GridContainer"/>,
|
||||
/// properly handles layout constraints so putting word-wrapping <see cref="RichTextLabel"/> in it should work.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All children are automatically laid out in <see cref="Columns"/> columns.
|
||||
/// The first control is in the top left, laid out per row from there.
|
||||
/// </remarks>
|
||||
[Virtual]
|
||||
public class TableContainer : Container
|
||||
{
|
||||
private int _columns = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The absolute minimum width a column can be forced to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If a column *asks* for less width than this (small contents), it can still be smaller.
|
||||
/// But if it asks for more it cannot go below this width.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public float MinForcedColumnWidth { get; set; } = 50;
|
||||
|
||||
// Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
|
||||
private ColumnData[] _columnDataCache = [];
|
||||
private RowData[] _rowDataCache = [];
|
||||
|
||||
/// <summary>
|
||||
/// How many columns should be displayed.
|
||||
/// </summary>
|
||||
public int Columns
|
||||
{
|
||||
get => _columns;
|
||||
set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
|
||||
|
||||
_columns = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
ResetCachedArrays();
|
||||
|
||||
// Do a first pass measuring all child controls as if they're given infinite space.
|
||||
// This gives us a maximum width the columns want, which we use to proportion them later.
|
||||
var columnIdx = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
ref var column = ref _columnDataCache[columnIdx];
|
||||
|
||||
child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
|
||||
column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
|
||||
|
||||
columnIdx += 1;
|
||||
if (columnIdx == _columns)
|
||||
columnIdx = 0;
|
||||
}
|
||||
|
||||
// Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
|
||||
var totalMinWidth = 0f;
|
||||
var totalMaxWidth = 0f;
|
||||
var totalSlack = 0f;
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
|
||||
column.Slack = column.MaxWidth - column.MinWidth;
|
||||
|
||||
totalMinWidth += column.MinWidth;
|
||||
totalMaxWidth += column.MaxWidth;
|
||||
totalSlack += column.Slack;
|
||||
}
|
||||
|
||||
if (totalMaxWidth <= availableSize.X)
|
||||
{
|
||||
// We want less horizontal space than we're given. Huh, that's convenient.
|
||||
// Just set assigned width to be however much they asked for.
|
||||
// We could probably skip the second measure pass in this scenario,
|
||||
// but that's just an optimization, so I don't care right now.
|
||||
//
|
||||
// There's probably a very clever way to make this behavior work with the else block of logic,
|
||||
// just by fiddling with the math.
|
||||
// I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
column.AssignedWidth = column.MaxWidth;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't have enough horizontal space,
|
||||
// at least without causing *some* sort of word wrapping (assuming text contents).
|
||||
//
|
||||
// Assign horizontal space proportional to the wanted maximum size of the columns.
|
||||
var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
var slackRatio = column.Slack / totalSlack;
|
||||
column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Go over controls for a second measuring pass, this time giving them their assigned measure width.
|
||||
// This will give us a height to slot into per-row data.
|
||||
// We still measure assuming infinite vertical space.
|
||||
// This control can't properly handle being constrained on the Y axis.
|
||||
columnIdx = 0;
|
||||
var rowIdx = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
ref var column = ref _columnDataCache[columnIdx];
|
||||
ref var row = ref _rowDataCache[rowIdx];
|
||||
|
||||
child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
|
||||
row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
|
||||
|
||||
columnIdx += 1;
|
||||
if (columnIdx == _columns)
|
||||
{
|
||||
columnIdx = 0;
|
||||
rowIdx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sum up height of all rows to get final measured table height.
|
||||
var totalHeight = 0f;
|
||||
for (var r = 0; r < _rowDataCache.Length; r++)
|
||||
{
|
||||
ref var row = ref _rowDataCache[r];
|
||||
totalHeight += row.MeasuredHeight;
|
||||
}
|
||||
|
||||
return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
// TODO: Expand to fit given vertical space.
|
||||
|
||||
// Calculate MinWidth and Slack sums again from column data.
|
||||
// We could've cached these from measure but whatever.
|
||||
var totalMinWidth = 0f;
|
||||
var totalSlack = 0f;
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
totalMinWidth += column.MinWidth;
|
||||
totalSlack += column.Slack;
|
||||
}
|
||||
|
||||
// Calculate new width based on final given size, also assign horizontal positions of all columns.
|
||||
var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
|
||||
var xPos = 0f;
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
var slackRatio = column.Slack / totalSlack;
|
||||
column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
|
||||
column.ArrangedX = xPos;
|
||||
|
||||
xPos += column.ArrangedWidth;
|
||||
}
|
||||
|
||||
// Do actual arrangement row-by-row.
|
||||
var arrangeY = 0f;
|
||||
for (var r = 0; r < _rowDataCache.Length; r++)
|
||||
{
|
||||
ref var row = ref _rowDataCache[r];
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
var index = c + r * _columns;
|
||||
|
||||
if (index >= ChildCount) // Quit early if we don't actually fill out the row.
|
||||
break;
|
||||
var child = GetChild(c + r * _columns);
|
||||
|
||||
child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
|
||||
}
|
||||
|
||||
arrangeY += row.MeasuredHeight;
|
||||
}
|
||||
|
||||
return finalSize with { Y = arrangeY };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure cached array space is allocated to correct size and is reset to a clean slate.
|
||||
/// </summary>
|
||||
private void ResetCachedArrays()
|
||||
{
|
||||
// 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
|
||||
|
||||
if (_columnDataCache.Length != _columns)
|
||||
_columnDataCache = new ColumnData[_columns];
|
||||
|
||||
Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
|
||||
|
||||
var rowCount = ChildCount / _columns;
|
||||
if (ChildCount % _columns != 0)
|
||||
rowCount += 1;
|
||||
|
||||
if (rowCount != _rowDataCache.Length)
|
||||
_rowDataCache = new RowData[rowCount];
|
||||
|
||||
Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-column data used during layout.
|
||||
/// </summary>
|
||||
private struct ColumnData
|
||||
{
|
||||
// Measure data.
|
||||
|
||||
/// <summary>
|
||||
/// The maximum width any control in this column wants, if given infinite space.
|
||||
/// Maximum of all controls on the column.
|
||||
/// </summary>
|
||||
public float MaxWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum width this column may be given.
|
||||
/// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
|
||||
/// </summary>
|
||||
public float MinWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Difference between max and min width; how much this column can expand from its minimum.
|
||||
/// </summary>
|
||||
public float Slack;
|
||||
|
||||
/// <summary>
|
||||
/// How much horizontal space this column was assigned at measure time.
|
||||
/// </summary>
|
||||
public float AssignedWidth;
|
||||
|
||||
// Arrange data.
|
||||
|
||||
/// <summary>
|
||||
/// How much horizontal space this column was assigned at arrange time.
|
||||
/// </summary>
|
||||
public float ArrangedWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The horizontal position this column was assigned at arrange time.
|
||||
/// </summary>
|
||||
public float ArrangedX;
|
||||
}
|
||||
|
||||
private struct RowData
|
||||
{
|
||||
// Measure data.
|
||||
|
||||
/// <summary>
|
||||
/// How much height the tallest control on this row was measured at,
|
||||
/// measuring for infinite vertical space but assigned column width.
|
||||
/// </summary>
|
||||
public float MeasuredHeight;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user