]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Guidebook Tables (#28484)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sun, 2 Jun 2024 03:58:33 +0000 (23:58 -0400)
committerGitHub <noreply@github.com>
Sun, 2 Jun 2024 03:58:33 +0000 (20:58 -0700)
* PJB's cool table control (it probably doesn't work)

* ok wait wrong file

* Guidebook Tables

Content.Client/Guidebook/Richtext/Box.cs
Content.Client/Guidebook/Richtext/ColorBox.cs [new file with mode: 0644]
Content.Client/Guidebook/Richtext/Table.cs [new file with mode: 0644]
Content.Client/UserInterface/Controls/TableContainer.cs [new file with mode: 0644]

index ecf6cb21f70a741609bb57bff75107728e866cf7..6e18ad9c57547b8d99f2f4664b16e68b5a5c3c67 100644 (file)
@@ -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
diff --git a/Content.Client/Guidebook/Richtext/ColorBox.cs b/Content.Client/Guidebook/Richtext/ColorBox.cs
new file mode 100644 (file)
index 0000000..84de300
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Content.Client/Guidebook/Richtext/Table.cs b/Content.Client/Guidebook/Richtext/Table.cs
new file mode 100644 (file)
index 0000000..b6923c3
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Content.Client/UserInterface/Controls/TableContainer.cs b/Content.Client/UserInterface/Controls/TableContainer.cs
new file mode 100644 (file)
index 0000000..3e8d476
--- /dev/null
@@ -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;
+    }
+}