ModernUO/Projects/UOContent/Gumps/Base/GridLayout/GridCalculator.cs
Kamron Batman bc6735bd23
feat: Adds grid support for the DynamicGump system (#2306)
### Summary 

- Adds a new grid layout system for DynamicGump with zero heap allocations
 - Migrates SpawnerControllerGump from legacy GumpGrid to DynamicGump
 - Migrates CommandListGump from BaseGridGump to DynamicGump
 - Removes legacy GumpGrid (no longer needed)

 ### New Grid Layout Components

 | Component | Purpose |
 |-----------|---------|
 | `GridCell` | Value type for cell bounds (x, y, width, height) |
 | `GridSizeSpec` | Parses CSS-like sizing specs ("10*", "*", "100") |
 | `GridCalculator` | Computes track positions using stackalloc |
 | `ListViewLayout` | Pagination + column layout for list views |
 | `GridBuilderExtensions` | Extension methods accepting `GridCell` |
 | `GridEntryStyle` | Styling properties for BaseGridGump migration |
 | `GridEntryExtensions` | Entry methods matching BaseGridGump patterns |

 ### Memory Impact

 | Component | Legacy | New |
 |-----------|--------|-----|
 | Grid storage | ~200 bytes heap | 0 bytes (stackalloc) |
 | Column/Row lists | ~400 bytes heap | ~128 bytes stack |
 | ListView | ~300 bytes heap | ~64 bytes stack |
 | **Total per render** | **~900 bytes heap** | **0 bytes heap** |

 ### Test plan

 - [x] All 21 gump tests pass
 - [x] Build succeeds with no warnings
 - [ ] In-game verification of SpawnerControllerGump
 - [ ] In-game verification of CommandListGump (HelpInfo command)
2026-01-03 10:12:22 -08:00

242 lines
8.4 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridCalculator.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
namespace Server.Gumps;
/// <summary>
/// Static helper for calculating grid cell positions without heap allocations.
/// All results are written to caller-provided spans using stackalloc.
/// </summary>
public static class GridCalculator
{
/// <summary>
/// Maximum supported columns or rows per grid.
/// </summary>
public const int MaxTracks = 32;
/// <summary>
/// Computes track sizes from size specifications.
/// Writes positions and sizes to the provided spans.
/// </summary>
/// <param name="specs">The size specifications for each track.</param>
/// <param name="totalSize">The total available size (width or height).</param>
/// <param name="origin">The starting position (x or y offset).</param>
/// <param name="positions">Output: position of each track.</param>
/// <param name="sizes">Output: size of each track.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeTrackSizes(
ReadOnlySpan<GridSizeSpec> specs,
int totalSize,
int origin,
Span<int> positions,
Span<int> sizes
)
{
var trackCount = specs.Length;
// First pass: calculate absolute/percent sizes and count star tracks
var remainingSize = totalSize;
var starCount = 0;
for (var i = 0; i < trackCount; i++)
{
var spec = specs[i];
switch (spec.Type)
{
case GridSizeType.Absolute:
{
sizes[i] = spec.Value;
remainingSize -= spec.Value;
break;
}
case GridSizeType.Percent:
{
sizes[i] = totalSize * spec.Value / 100;
remainingSize -= sizes[i];
break;
}
case GridSizeType.Star:
{
starCount++;
sizes[i] = -1; // Mark as star for second pass
break;
}
}
}
// Second pass: distribute remaining space to star tracks
if (starCount > 0 && remainingSize > 0)
{
var starSize = remainingSize / starCount;
for (var i = 0; i < trackCount; i++)
{
if (sizes[i] == -1)
{
sizes[i] = starSize;
}
}
}
else
{
// No star tracks or no remaining space - set any marked stars to 0
for (var i = 0; i < trackCount; i++)
{
if (sizes[i] < 0)
{
sizes[i] = 0;
}
}
}
// Third pass: calculate positions
var pos = origin;
for (var i = 0; i < trackCount; i++)
{
positions[i] = pos;
pos += sizes[i];
}
}
/// <summary>
/// Computes uniform track sizes (equal-sized cells).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ComputeUniformTrackSizes(
int trackCount,
int totalSize,
int origin,
Span<int> positions,
Span<int> sizes
)
{
var trackSize = totalSize / trackCount;
var pos = origin;
for (var i = 0; i < trackCount; i++)
{
positions[i] = pos;
sizes[i] = trackSize;
pos += trackSize;
}
}
/// <summary>
/// Parses a size specification string and computes track sizes.
/// </summary>
/// <param name="sizeSpec">Space-separated size specification (e.g., "10* * 100").</param>
/// <param name="totalSize">The total available size.</param>
/// <param name="origin">The starting position.</param>
/// <param name="positions">Output: position of each track.</param>
/// <param name="sizes">Output: size of each track.</param>
/// <returns>The number of tracks computed.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeFromSpec(
ReadOnlySpan<char> sizeSpec,
int totalSize,
int origin,
Span<int> positions,
Span<int> sizes
) => ComputeFromSpec(sizeSpec, totalSize, origin, 0, positions, sizes);
/// <summary>
/// Parses a size specification string and computes track sizes with gaps between tracks.
/// </summary>
/// <param name="sizeSpec">Space-separated size specification (e.g., "10* * 100").</param>
/// <param name="totalSize">The total available size.</param>
/// <param name="origin">The starting position.</param>
/// <param name="gap">The gap size between tracks.</param>
/// <param name="positions">Output: position of each track.</param>
/// <param name="sizes">Output: size of each track.</param>
/// <returns>The number of tracks computed.</returns>
public static int ComputeFromSpec(
ReadOnlySpan<char> sizeSpec,
int totalSize,
int origin,
int gap,
Span<int> positions,
Span<int> sizes)
{
Span<GridSizeSpec> specs = stackalloc GridSizeSpec[MaxTracks];
var trackCount = GridSizeSpec.ParseAll(sizeSpec, specs);
if (trackCount > 0)
{
// Subtract total gap space from available size before computing track sizes
var totalGapSpace = gap * (trackCount - 1);
var availableSize = totalSize - totalGapSpace;
ComputeTrackSizes(specs[..trackCount], availableSize, origin, positions, sizes);
// Adjust positions to account for gaps
if (gap > 0)
{
for (var i = 1; i < trackCount; i++)
{
positions[i] += gap * i;
}
}
}
return trackCount;
}
/// <summary>
/// Gets a cell at the specified column and row from pre-computed grid arrays.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridCell GetCell(
ReadOnlySpan<int> columnPositions,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowPositions,
ReadOnlySpan<int> rowHeights,
int column,
int row
) => new(columnPositions[column], rowPositions[row], columnWidths[column], rowHeights[row]);
/// <summary>
/// Gets a cell spanning multiple columns and/or rows from pre-computed grid arrays.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridCell GetCell(
ReadOnlySpan<int> columnPositions,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowPositions,
ReadOnlySpan<int> rowHeights,
int column,
int row,
int columnSpan,
int rowSpan
)
{
var width = 0;
var endCol = Math.Min(column + columnSpan, columnWidths.Length);
for (var c = column; c < endCol; c++)
{
width += columnWidths[c];
}
var height = 0;
var endRow = Math.Min(row + rowSpan, rowHeights.Length);
for (var r = row; r < endRow; r++)
{
height += rowHeights[r];
}
return new GridCell(columnPositions[column], rowPositions[row], width, height);
}
}