Skip to content

Commit 75bb8d3

Browse files
[Port] Port Guidebook Tables From Wizden PR #28484 (#1427)
# Description This ports the Guidebook Tables to allow \<Table\> and \<ColorBox\> embeds in the Guidebook. This just adds extra XML tags to use in rich-text. --- # TODO - [x] Cherry-Pick the PR. - [x] Tested to make sure it works. It does actively work. --- # Media <details><summary><h3>Guidebook Screenshot</h3></summary> <p> ![image](https://github.com/user-attachments/assets/289e4c72-dcef-4489-b89e-5a2d6367124f) </p> </details> NOTE: This screenshot was taken in the dev-environment. I just copy-pasted my SOP for Alert Levels to check it, since it uses both the \<Table\> and \<ColorBox\> identifiers. --- # Changelog :cl: - add: Added <Table> and <ColorBox> identifiers. Go wild in SOP! Co-authored-by: Nemanja <[email protected]>
1 parent b8074ea commit 75bb8d3

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed

Content.Client/Guidebook/Richtext/Box.cs

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out
1111
HorizontalExpand = true;
1212
control = this;
1313

14+
if (args.TryGetValue("Margin", out var margin))
15+
Margin = new Thickness(float.Parse(margin));
16+
1417
if (args.TryGetValue("Orientation", out var orientation))
1518
Orientation = Enum.Parse<LayoutOrientation>(orientation);
1619
else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using JetBrains.Annotations;
3+
using Robust.Client.Graphics;
4+
using Robust.Client.UserInterface;
5+
using Robust.Client.UserInterface.Controls;
6+
7+
namespace Content.Client.Guidebook.Richtext;
8+
9+
[UsedImplicitly]
10+
public sealed class ColorBox : PanelContainer, IDocumentTag
11+
{
12+
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
13+
{
14+
HorizontalExpand = true;
15+
VerticalExpand = true;
16+
control = this;
17+
18+
if (args.TryGetValue("Margin", out var margin))
19+
Margin = new Thickness(float.Parse(margin));
20+
21+
if (args.TryGetValue("HorizontalAlignment", out var halign))
22+
HorizontalAlignment = Enum.Parse<HAlignment>(halign);
23+
else
24+
HorizontalAlignment = HAlignment.Stretch;
25+
26+
if (args.TryGetValue("VerticalAlignment", out var valign))
27+
VerticalAlignment = Enum.Parse<VAlignment>(valign);
28+
else
29+
VerticalAlignment = VAlignment.Stretch;
30+
31+
var styleBox = new StyleBoxFlat();
32+
if (args.TryGetValue("Color", out var color))
33+
styleBox.BackgroundColor = Color.FromHex(color);
34+
35+
if (args.TryGetValue("OutlineThickness", out var outlineThickness))
36+
styleBox.BorderThickness = new Thickness(float.Parse(outlineThickness));
37+
else
38+
styleBox.BorderThickness = new Thickness(1);
39+
40+
if (args.TryGetValue("OutlineColor", out var outlineColor))
41+
styleBox.BorderColor = Color.FromHex(outlineColor);
42+
else
43+
styleBox.BorderColor = Color.White;
44+
45+
PanelOverride = styleBox;
46+
47+
return true;
48+
}
49+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Content.Client.UserInterface.Controls;
3+
using JetBrains.Annotations;
4+
using Robust.Client.UserInterface;
5+
6+
namespace Content.Client.Guidebook.Richtext;
7+
8+
[UsedImplicitly]
9+
public sealed class Table : TableContainer, IDocumentTag
10+
{
11+
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
12+
{
13+
HorizontalExpand = true;
14+
control = this;
15+
16+
if (!args.TryGetValue("Columns", out var columns) || !int.TryParse(columns, out var columnsCount))
17+
{
18+
Logger.Error("Guidebook tag \"Table\" does not specify required property \"Columns.\"");
19+
control = null;
20+
return false;
21+
}
22+
23+
Columns = columnsCount;
24+
25+
return true;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
using System.Numerics;
2+
using Robust.Client.UserInterface.Controls;
3+
4+
namespace Content.Client.UserInterface.Controls;
5+
6+
// 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
7+
// API stabilization and/or figuring out relation to GridContainer.
8+
// Grid layout is a complicated problem and I don't want to commit another half-baked thing into the engine.
9+
// It's probably sufficient for its use case (RichTextLabel tables for rules/guidebook).
10+
// Despite that, it's still better comment the shit half of you write on a regular basis.
11+
//
12+
// EMO: thank you PJB i was going to kill myself.
13+
14+
/// <summary>
15+
/// Displays children in a tabular grid. Unlike <see cref="GridContainer"/>,
16+
/// properly handles layout constraints so putting word-wrapping <see cref="RichTextLabel"/> in it should work.
17+
/// </summary>
18+
/// <remarks>
19+
/// All children are automatically laid out in <see cref="Columns"/> columns.
20+
/// The first control is in the top left, laid out per row from there.
21+
/// </remarks>
22+
[Virtual]
23+
public class TableContainer : Container
24+
{
25+
private int _columns = 1;
26+
27+
/// <summary>
28+
/// The absolute minimum width a column can be forced to.
29+
/// </summary>
30+
/// <remarks>
31+
/// <para>
32+
/// If a column *asks* for less width than this (small contents), it can still be smaller.
33+
/// But if it asks for more it cannot go below this width.
34+
/// </para>
35+
/// </remarks>
36+
public float MinForcedColumnWidth { get; set; } = 50;
37+
38+
// Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
39+
private ColumnData[] _columnDataCache = [];
40+
private RowData[] _rowDataCache = [];
41+
42+
/// <summary>
43+
/// How many columns should be displayed.
44+
/// </summary>
45+
public int Columns
46+
{
47+
get => _columns;
48+
set
49+
{
50+
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
51+
52+
_columns = value;
53+
}
54+
}
55+
56+
protected override Vector2 MeasureOverride(Vector2 availableSize)
57+
{
58+
ResetCachedArrays();
59+
60+
// Do a first pass measuring all child controls as if they're given infinite space.
61+
// This gives us a maximum width the columns want, which we use to proportion them later.
62+
var columnIdx = 0;
63+
foreach (var child in Children)
64+
{
65+
ref var column = ref _columnDataCache[columnIdx];
66+
67+
child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
68+
column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
69+
70+
columnIdx += 1;
71+
if (columnIdx == _columns)
72+
columnIdx = 0;
73+
}
74+
75+
// Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
76+
var totalMinWidth = 0f;
77+
var totalMaxWidth = 0f;
78+
var totalSlack = 0f;
79+
80+
for (var c = 0; c < _columns; c++)
81+
{
82+
ref var column = ref _columnDataCache[c];
83+
column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
84+
column.Slack = column.MaxWidth - column.MinWidth;
85+
86+
totalMinWidth += column.MinWidth;
87+
totalMaxWidth += column.MaxWidth;
88+
totalSlack += column.Slack;
89+
}
90+
91+
if (totalMaxWidth <= availableSize.X)
92+
{
93+
// We want less horizontal space than we're given. Huh, that's convenient.
94+
// Just set assigned width to be however much they asked for.
95+
// We could probably skip the second measure pass in this scenario,
96+
// but that's just an optimization, so I don't care right now.
97+
//
98+
// There's probably a very clever way to make this behavior work with the else block of logic,
99+
// just by fiddling with the math.
100+
// I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
101+
for (var c = 0; c < _columns; c++)
102+
{
103+
ref var column = ref _columnDataCache[c];
104+
105+
column.AssignedWidth = column.MaxWidth;
106+
}
107+
}
108+
else
109+
{
110+
// We don't have enough horizontal space,
111+
// at least without causing *some* sort of word wrapping (assuming text contents).
112+
//
113+
// Assign horizontal space proportional to the wanted maximum size of the columns.
114+
var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
115+
for (var c = 0; c < _columns; c++)
116+
{
117+
ref var column = ref _columnDataCache[c];
118+
119+
var slackRatio = column.Slack / totalSlack;
120+
column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
121+
}
122+
}
123+
124+
// Go over controls for a second measuring pass, this time giving them their assigned measure width.
125+
// This will give us a height to slot into per-row data.
126+
// We still measure assuming infinite vertical space.
127+
// This control can't properly handle being constrained on the Y axis.
128+
columnIdx = 0;
129+
var rowIdx = 0;
130+
foreach (var child in Children)
131+
{
132+
ref var column = ref _columnDataCache[columnIdx];
133+
ref var row = ref _rowDataCache[rowIdx];
134+
135+
child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
136+
row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
137+
138+
columnIdx += 1;
139+
if (columnIdx == _columns)
140+
{
141+
columnIdx = 0;
142+
rowIdx += 1;
143+
}
144+
}
145+
146+
// Sum up height of all rows to get final measured table height.
147+
var totalHeight = 0f;
148+
for (var r = 0; r < _rowDataCache.Length; r++)
149+
{
150+
ref var row = ref _rowDataCache[r];
151+
totalHeight += row.MeasuredHeight;
152+
}
153+
154+
return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
155+
}
156+
157+
protected override Vector2 ArrangeOverride(Vector2 finalSize)
158+
{
159+
// TODO: Expand to fit given vertical space.
160+
161+
// Calculate MinWidth and Slack sums again from column data.
162+
// We could've cached these from measure but whatever.
163+
var totalMinWidth = 0f;
164+
var totalSlack = 0f;
165+
166+
for (var c = 0; c < _columns; c++)
167+
{
168+
ref var column = ref _columnDataCache[c];
169+
totalMinWidth += column.MinWidth;
170+
totalSlack += column.Slack;
171+
}
172+
173+
// Calculate new width based on final given size, also assign horizontal positions of all columns.
174+
var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
175+
var xPos = 0f;
176+
for (var c = 0; c < _columns; c++)
177+
{
178+
ref var column = ref _columnDataCache[c];
179+
180+
var slackRatio = column.Slack / totalSlack;
181+
column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
182+
column.ArrangedX = xPos;
183+
184+
xPos += column.ArrangedWidth;
185+
}
186+
187+
// Do actual arrangement row-by-row.
188+
var arrangeY = 0f;
189+
for (var r = 0; r < _rowDataCache.Length; r++)
190+
{
191+
ref var row = ref _rowDataCache[r];
192+
193+
for (var c = 0; c < _columns; c++)
194+
{
195+
ref var column = ref _columnDataCache[c];
196+
var index = c + r * _columns;
197+
198+
if (index >= ChildCount) // Quit early if we don't actually fill out the row.
199+
break;
200+
var child = GetChild(c + r * _columns);
201+
202+
child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
203+
}
204+
205+
arrangeY += row.MeasuredHeight;
206+
}
207+
208+
return finalSize with { Y = arrangeY };
209+
}
210+
211+
/// <summary>
212+
/// Ensure cached array space is allocated to correct size and is reset to a clean slate.
213+
/// </summary>
214+
private void ResetCachedArrays()
215+
{
216+
// 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
217+
218+
if (_columnDataCache.Length != _columns)
219+
_columnDataCache = new ColumnData[_columns];
220+
221+
Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
222+
223+
var rowCount = ChildCount / _columns;
224+
if (ChildCount % _columns != 0)
225+
rowCount += 1;
226+
227+
if (rowCount != _rowDataCache.Length)
228+
_rowDataCache = new RowData[rowCount];
229+
230+
Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
231+
}
232+
233+
/// <summary>
234+
/// Per-column data used during layout.
235+
/// </summary>
236+
private struct ColumnData
237+
{
238+
// Measure data.
239+
240+
/// <summary>
241+
/// The maximum width any control in this column wants, if given infinite space.
242+
/// Maximum of all controls on the column.
243+
/// </summary>
244+
public float MaxWidth;
245+
246+
/// <summary>
247+
/// The minimum width this column may be given.
248+
/// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
249+
/// </summary>
250+
public float MinWidth;
251+
252+
/// <summary>
253+
/// Difference between max and min width; how much this column can expand from its minimum.
254+
/// </summary>
255+
public float Slack;
256+
257+
/// <summary>
258+
/// How much horizontal space this column was assigned at measure time.
259+
/// </summary>
260+
public float AssignedWidth;
261+
262+
// Arrange data.
263+
264+
/// <summary>
265+
/// How much horizontal space this column was assigned at arrange time.
266+
/// </summary>
267+
public float ArrangedWidth;
268+
269+
/// <summary>
270+
/// The horizontal position this column was assigned at arrange time.
271+
/// </summary>
272+
public float ArrangedX;
273+
}
274+
275+
private struct RowData
276+
{
277+
// Measure data.
278+
279+
/// <summary>
280+
/// How much height the tallest control on this row was measured at,
281+
/// measuring for infinite vertical space but assigned column width.
282+
/// </summary>
283+
public float MeasuredHeight;
284+
}
285+
}

0 commit comments

Comments
 (0)