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)
This commit is contained in:
Kamron Batman 2026-01-03 10:12:22 -08:00 committed by GitHub
parent a354f79f54
commit bc6735bd23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2579 additions and 1015 deletions

View file

@ -0,0 +1,512 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridBuilderExtensions.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;
using Server.Buffers;
namespace Server.Gumps;
/// <summary>
/// Extension methods for DynamicGumpBuilder that accept GridCell for positioning.
/// Provides ergonomic grid-based gump building with zero allocations.
/// </summary>
public static class GridBuilderExtensions
{
extension(ref DynamicGumpBuilder builder)
{
/// <summary>
/// Adds a background element filling the entire cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddBackground(in GridCell cell, int gumpId) =>
builder.AddBackground(cell.X, cell.Y, cell.Width, cell.Height, gumpId);
/// <summary>
/// Adds an alpha region filling the entire cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddAlphaRegion(in GridCell cell) =>
builder.AddAlphaRegion(cell.X, cell.Y, cell.Width, cell.Height);
/// <summary>
/// Adds a tiled image filling the entire cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddImageTiled(in GridCell cell, int gumpId) =>
builder.AddImageTiled(cell.X, cell.Y, cell.Width, cell.Height, gumpId);
/// <summary>
/// Adds an image at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddImage(
in GridCell cell,
int gumpId,
int hue = 0,
int offsetX = 0,
int offsetY = 0
) => builder.AddImage(cell.X + offsetX, cell.Y + offsetY, gumpId, hue);
/// <summary>
/// Adds an item display at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddItem(
in GridCell cell,
int itemId,
int hue = 0,
int offsetX = 0,
int offsetY = 0
) => builder.AddItem(cell.X + offsetX, cell.Y + offsetY, itemId, hue);
/// <summary>
/// Adds a label at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddLabel(
in GridCell cell,
int hue,
ReadOnlySpan<char> text,
int offsetX = 0,
int offsetY = 0
) => builder.AddLabel(cell.X + offsetX, cell.Y + offsetY, hue, text);
/// <summary>
/// Adds a label at the cell position with optional offset using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddLabel(
in GridCell cell,
int hue,
ref RawInterpolatedStringHandler handler,
int offsetX = 0,
int offsetY = 0
)
{
builder.AddLabel(cell.X + offsetX, cell.Y + offsetY, hue, handler.Text);
handler.Clear();
}
/// <summary>
/// Adds a cropped label filling the cell with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddLabelCropped(
in GridCell cell,
int hue,
ReadOnlySpan<char> text,
int offsetX = 0,
int offsetY = 0
) => builder.AddLabelCropped(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
hue,
text
);
/// <summary>
/// Adds a cropped label filling the cell with optional offset using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddLabelCropped(
in GridCell cell,
int hue,
ref RawInterpolatedStringHandler handler,
int offsetX = 0,
int offsetY = 0
)
{
builder.AddLabelCropped(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
hue,
handler.Text
);
handler.Clear();
}
/// <summary>
/// Adds HTML text filling the entire cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtml(
in GridCell cell,
ReadOnlySpan<char> text,
ReadOnlySpan<char> color = default,
int size = -1,
byte fontStyle = 0,
TextAlignment align = TextAlignment.Left,
bool background = false,
bool scrollbar = false
) => builder.AddHtml(
cell.X,
cell.Y,
cell.Width,
cell.Height,
text,
color,
size,
fontStyle,
align,
background,
scrollbar
);
/// <summary>
/// Adds HTML text filling the entire cell using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtml(
in GridCell cell,
ref RawInterpolatedStringHandler handler,
ReadOnlySpan<char> color = default,
int size = -1,
byte fontStyle = 0,
TextAlignment align = TextAlignment.Left,
bool background = false,
bool scrollbar = false
)
{
builder.AddHtml(
cell.X,
cell.Y,
cell.Width,
cell.Height,
handler.Text,
color,
size,
fontStyle,
align,
background,
scrollbar
);
handler.Clear();
}
/// <summary>
/// Adds HTML text filling the cell with offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtml(
in GridCell cell,
ReadOnlySpan<char> text,
int offsetX,
int offsetY,
ReadOnlySpan<char> color = default,
int size = -1,
byte fontStyle = 0,
TextAlignment align = TextAlignment.Left,
bool background = false,
bool scrollbar = false
) => builder.AddHtml(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
text,
color,
size,
fontStyle,
align,
background,
scrollbar
);
/// <summary>
/// Adds HTML text filling the cell with offset using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtml(
in GridCell cell,
ref RawInterpolatedStringHandler handler,
int offsetX,
int offsetY,
ReadOnlySpan<char> color = default,
int size = -1,
byte fontStyle = 0,
TextAlignment align = TextAlignment.Left,
bool background = false,
bool scrollbar = false
)
{
builder.AddHtml(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
handler.Text,
color,
size,
fontStyle,
align,
background,
scrollbar
);
handler.Clear();
}
/// <summary>
/// Adds a localized HTML element filling the cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtmlLocalized(
in GridCell cell,
int number,
bool background = false,
bool scrollbar = false
) => builder.AddHtmlLocalized(cell.X, cell.Y, cell.Width, cell.Height, number, background, scrollbar);
/// <summary>
/// Adds a localized HTML element with color.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtmlLocalized(
in GridCell cell,
int number,
int color,
bool background = false,
bool scrollbar = false
) => builder.AddHtmlLocalized(cell.X, cell.Y, cell.Width, cell.Height, number, color, background, scrollbar);
/// <summary>
/// Adds a localized HTML element with arguments.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtmlLocalized(
in GridCell cell,
int number,
ReadOnlySpan<char> args,
int color,
bool background = false,
bool scrollbar = false
) => builder.AddHtmlLocalized(cell.X, cell.Y, cell.Width, cell.Height, number, args, color, background, scrollbar);
/// <summary>
/// Adds a localized HTML element with arguments using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddHtmlLocalized(
in GridCell cell,
int number,
ref RawInterpolatedStringHandler handler,
int color,
bool background = false,
bool scrollbar = false
) => builder.AddHtmlLocalized(cell.X, cell.Y, cell.Width, cell.Height, number, ref handler, color, background, scrollbar);
/// <summary>
/// Adds a button at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddButton(
in GridCell cell,
int normalId,
int pressedId,
int buttonId,
GumpButtonType type = GumpButtonType.Reply,
int param = 0,
int offsetX = 0,
int offsetY = 0
) => builder.AddButton(cell.X + offsetX, cell.Y + offsetY, normalId, pressedId, buttonId, type, param);
/// <summary>
/// Adds a checkbox at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddCheckbox(
in GridCell cell,
int inactiveId,
int activeId,
bool selected,
int switchId,
int offsetX = 0,
int offsetY = 0
) => builder.AddCheckbox(cell.X + offsetX, cell.Y + offsetY, inactiveId, activeId, selected, switchId);
/// <summary>
/// Adds a radio button at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRadio(
in GridCell cell,
int inactiveId,
int activeId,
bool selected,
int switchId,
int offsetX = 0,
int offsetY = 0
) => builder.AddRadio(cell.X + offsetX, cell.Y + offsetY, inactiveId, activeId, selected, switchId);
/// <summary>
/// Adds a text entry filling the cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntry(
in GridCell cell,
int hue,
int entryId,
ReadOnlySpan<char> initialText = default
) => builder.AddTextEntry(cell.X, cell.Y, cell.Width, cell.Height, hue, entryId, initialText);
/// <summary>
/// Adds a text entry filling the cell using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntry(
in GridCell cell,
int hue,
int entryId,
ref RawInterpolatedStringHandler handler
)
{
builder.AddTextEntry(cell.X, cell.Y, cell.Width, cell.Height, hue, entryId, handler.Text);
handler.Clear();
}
/// <summary>
/// Adds a text entry with offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntry(
in GridCell cell,
int hue,
int entryId,
int offsetX,
int offsetY,
ReadOnlySpan<char> initialText = default
) => builder.AddTextEntry(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
hue,
entryId,
initialText
);
/// <summary>
/// Adds a text entry with offset using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntry(
in GridCell cell,
int hue,
int entryId,
int offsetX,
int offsetY,
ref RawInterpolatedStringHandler handler
)
{
builder.AddTextEntry(
cell.X + offsetX,
cell.Y + offsetY,
cell.Width - offsetX,
cell.Height - offsetY,
hue,
entryId,
handler.Text
);
handler.Clear();
}
/// <summary>
/// Adds a limited text entry filling the cell.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntryLimited(
in GridCell cell,
int hue,
int entryId,
int size,
ReadOnlySpan<char> initialText = default
) => builder.AddTextEntryLimited(cell.X, cell.Y, cell.Width, cell.Height, hue, entryId, initialText, size);
/// <summary>
/// Adds a limited text entry filling the cell using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddTextEntryLimited(
in GridCell cell,
int hue,
int entryId,
int size,
ref RawInterpolatedStringHandler handler
)
{
builder.AddTextEntryLimited(cell.X, cell.Y, cell.Width, cell.Height, hue, entryId, handler.Text, size);
handler.Clear();
}
/// <summary>
/// Adds an image-tiled button at the cell position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddImageTiledButton(
in GridCell cell,
int normalId,
int pressedId,
int buttonId,
GumpButtonType type,
int param,
int itemId,
int hue,
int localizedTooltip = -1
) => builder.AddImageTiledButton(
cell.X,
cell.Y,
normalId,
pressedId,
buttonId,
type,
param,
itemId,
hue,
cell.Width,
cell.Height,
localizedTooltip
);
/// <summary>
/// Adds a sprite image using the cell dimensions.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddSpriteImage(
in GridCell cell,
int gumpId,
int sx,
int sy
) => builder.AddSpriteImage(cell.X, cell.Y, gumpId, cell.Width, cell.Height, sx, sy);
/// <summary>
/// Adds a sprite image at the cell position with optional offset.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddSpriteImage(
in GridCell cell,
int gumpId,
int width,
int height,
int sx,
int sy,
int offsetX = 0,
int offsetY = 0
) => builder.AddSpriteImage(cell.X + offsetX, cell.Y + offsetY, gumpId, width, height, sx, sy);
}
}

View file

@ -0,0 +1,242 @@
/*************************************************************************
* 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);
}
}

View file

@ -0,0 +1,114 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridCell.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.Runtime.CompilerServices;
namespace Server.Gumps;
/// <summary>
/// Represents a pre-calculated cell position within a grid layout.
/// This is a value type with zero heap allocations.
/// </summary>
public ref struct GridCell
{
public readonly int X;
public readonly int Y;
public readonly int Width;
public readonly int Height;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
/// <summary>
/// Gets the right edge X coordinate (X + Width).
/// </summary>
public int Right
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => X + Width;
}
/// <summary>
/// Gets the bottom edge Y coordinate (Y + Height).
/// </summary>
public int Bottom
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Y + Height;
}
/// <summary>
/// Gets the horizontal center X coordinate.
/// </summary>
public int CenterX
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => X + Width / 2;
}
/// <summary>
/// Gets the vertical center Y coordinate.
/// </summary>
public int CenterY
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Y + Height / 2;
}
/// <summary>
/// Creates a new cell with uniform margin inset on all sides.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell Inset(int margin) =>
new(X + margin, Y + margin, Width - margin * 2, Height - margin * 2);
/// <summary>
/// Creates a new cell with separate horizontal and vertical margins.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell Inset(int horizontal, int vertical) =>
new(X + horizontal, Y + vertical, Width - horizontal * 2, Height - vertical * 2);
/// <summary>
/// Creates a new cell with individual margins for each side.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell Inset(int left, int top, int right, int bottom) =>
new(X + left, Y + top, Width - left - right, Height - top - bottom);
/// <summary>
/// Creates a new cell with an offset applied to the position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell Offset(int offsetX, int offsetY) => new(X + offsetX, Y + offsetY, Width, Height);
/// <summary>
/// Creates a new cell with a different width.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell WithWidth(int width) => new(X, Y, width, Height);
/// <summary>
/// Creates a new cell with a different height.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell WithHeight(int height) => new(X, Y, Width, height);
}

View file

@ -0,0 +1,222 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridEntryExtensions.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;
using Server.Buffers;
namespace Server.Gumps;
/// <summary>
/// Extension methods for DynamicGumpBuilder that provide BaseGridGump-style entry methods.
/// These combine a tiled background with content, matching the AddEntry* pattern.
/// </summary>
public static class GridEntryExtensions
{
extension(ref DynamicGumpBuilder builder)
{
/// <summary>
/// Adds an entry with label (EntryGumpID background + cropped label).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryLabel(
in GridCell cell,
in GridEntryStyle style,
ReadOnlySpan<char> text
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddLabelCropped(cell, style.TextHue, text, style.TextOffsetX);
}
/// <summary>
/// Adds an entry with label using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryLabel(
in GridCell cell,
in GridEntryStyle style,
ref RawInterpolatedStringHandler handler
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddLabelCropped(cell, style.TextHue, ref handler, style.TextOffsetX);
}
/// <summary>
/// Adds an entry with HTML (EntryGumpID background + HTML text).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryHtml(
in GridCell cell,
in GridEntryStyle style,
ReadOnlySpan<char> text
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddHtml(cell, text, style.TextOffsetX, 0);
}
/// <summary>
/// Adds an entry with HTML using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryHtml(
in GridCell cell,
in GridEntryStyle style,
ref RawInterpolatedStringHandler handler
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddHtml(cell, ref handler, style.TextOffsetX, 0);
}
/// <summary>
/// Adds a header entry (HeaderGumpID background only).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryHeader(
in GridCell cell,
in GridEntryStyle style
)
{
builder.AddImageTiled(cell, style.HeaderGumpID);
}
/// <summary>
/// Adds an entry with a centered button (HeaderGumpID background + centered button).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryButton(
in GridCell cell,
in GridEntryStyle style,
int normalId,
int pressedId,
int buttonId,
int buttonWidth,
int buttonHeight
)
{
builder.AddImageTiled(cell, style.HeaderGumpID);
builder.AddButton(
cell, normalId, pressedId, buttonId,
offsetX: (cell.Width - buttonWidth) / 2,
offsetY: (cell.Height - buttonHeight) / 2
);
}
/// <summary>
/// Adds an entry with arrow left button.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryArrowLeft(
in GridCell cell,
in GridEntryStyle style,
int buttonId
)
{
builder.AddEntryButton(
cell, style,
GridEntryStyle.ArrowLeftID1,
GridEntryStyle.ArrowLeftID2,
buttonId,
GridEntryStyle.ArrowWidth,
GridEntryStyle.ArrowHeight
);
}
/// <summary>
/// Adds an entry with arrow right button.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryArrowRight(
in GridCell cell,
in GridEntryStyle style,
int buttonId
)
{
builder.AddEntryButton(
cell, style,
GridEntryStyle.ArrowRightID1,
GridEntryStyle.ArrowRightID2,
buttonId,
GridEntryStyle.ArrowWidth,
GridEntryStyle.ArrowHeight
);
}
/// <summary>
/// Adds an entry with text input (EntryGumpID background + text entry).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryText(
in GridCell cell,
in GridEntryStyle style,
int entryId,
ReadOnlySpan<char> initialText = default
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddTextEntry(cell, style.TextHue, entryId, style.TextOffsetX, 0, initialText);
}
/// <summary>
/// Adds an entry with text input using interpolated string.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryText(
in GridCell cell,
in GridEntryStyle style,
int entryId,
ref RawInterpolatedStringHandler handler
)
{
builder.AddImageTiled(cell, style.EntryGumpID);
builder.AddTextEntry(cell, style.TextHue, entryId, style.TextOffsetX, 0, ref handler);
}
/// <summary>
/// Adds a blank line (BackGumpID + 4 background across full width).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEntryBlank(
in GridCell cell,
in GridEntryStyle style
)
{
builder.AddImageTiled(cell, style.BackGumpID + 4);
}
/// <summary>
/// Adds the standard grid background (outer background + inner offset region).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddGridBackground(
int width,
int height,
in GridEntryStyle style
)
{
builder.AddBackground(0, 0, width, height, style.BackGumpID);
builder.AddImageTiled(
style.BorderSize,
style.BorderSize,
width - style.BorderSize * 2,
height - style.BorderSize * 2,
style.OffsetGumpID
);
}
}
}

View file

@ -0,0 +1,165 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridEntryStyle.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/>. *
*************************************************************************/
namespace Server.Gumps;
/// <summary>
/// Defines the visual style for grid-based entry layouts.
/// Mirrors the virtual properties from BaseGridGump for easy migration.
/// </summary>
public readonly struct GridEntryStyle
{
/// <summary>
/// Default style matching BaseGridGump defaults.
/// </summary>
public static readonly GridEntryStyle Default = new(
entryGumpID: 0x0BBC,
headerGumpID: 0x0E14,
offsetGumpID: 0x0A40,
backGumpID: 0x13BE,
textHue: 0,
textOffsetX: 2,
entryHeight: 20,
borderSize: 10,
offsetSize: 1
);
/// <summary>
/// Background gump ID for data entry cells.
/// </summary>
public readonly int EntryGumpID;
/// <summary>
/// Background gump ID for header/button cells.
/// </summary>
public readonly int HeaderGumpID;
/// <summary>
/// Gump ID for the offset/separator region.
/// </summary>
public readonly int OffsetGumpID;
/// <summary>
/// Gump ID for the main background.
/// </summary>
public readonly int BackGumpID;
/// <summary>
/// Text hue for labels.
/// </summary>
public readonly int TextHue;
/// <summary>
/// Horizontal offset for text within cells.
/// </summary>
public readonly int TextOffsetX;
/// <summary>
/// Height of each entry row.
/// </summary>
public readonly int EntryHeight;
/// <summary>
/// Size of the outer border.
/// </summary>
public readonly int BorderSize;
/// <summary>
/// Size of the offset/gap between cells.
/// </summary>
public readonly int OffsetSize;
// Arrow button constants (matching BaseGridGump)
public const int ArrowLeftID1 = 0x15E3;
public const int ArrowLeftID2 = 0x15E7;
public const int ArrowRightID1 = 0x15E1;
public const int ArrowRightID2 = 0x15E5;
public const int ArrowWidth = 16;
public const int ArrowHeight = 16;
public GridEntryStyle(
int entryGumpID,
int headerGumpID,
int offsetGumpID,
int backGumpID,
int textHue,
int textOffsetX,
int entryHeight,
int borderSize,
int offsetSize
)
{
EntryGumpID = entryGumpID;
HeaderGumpID = headerGumpID;
OffsetGumpID = offsetGumpID;
BackGumpID = backGumpID;
TextHue = textHue;
TextOffsetX = textOffsetX;
EntryHeight = entryHeight;
BorderSize = borderSize;
OffsetSize = offsetSize;
}
/// <summary>
/// Creates a copy with modified values.
/// </summary>
public GridEntryStyle With(
int entryGumpID = -1,
int headerGumpID = -1,
int offsetGumpID = -1,
int backGumpID = -1,
int textHue = -1,
int textOffsetX = -1,
int entryHeight = -1,
int borderSize = -1,
int offsetSize = -1
) => new(
entryGumpID < 0 ? EntryGumpID : entryGumpID,
headerGumpID < 0 ? HeaderGumpID : headerGumpID,
offsetGumpID < 0 ? OffsetGumpID : offsetGumpID,
backGumpID < 0 ? BackGumpID : backGumpID,
textHue < 0 ? TextHue : textHue,
textOffsetX < 0 ? TextOffsetX : textOffsetX,
entryHeight < 0 ? EntryHeight : entryHeight,
borderSize < 0 ? BorderSize : borderSize,
offsetSize < 0 ? OffsetSize : offsetSize
);
/// <summary>
/// Calculates the content origin X (inside borders).
/// </summary>
public int ContentOriginX => BorderSize + OffsetSize;
/// <summary>
/// Calculates the content origin Y (inside borders).
/// </summary>
public int ContentOriginY => BorderSize + OffsetSize;
/// <summary>
/// Calculates total width given content width.
/// </summary>
public int GetTotalWidth(int contentWidth) => contentWidth + BorderSize * 2 + OffsetSize * 2;
/// <summary>
/// Calculates total height given number of rows.
/// </summary>
public int GetTotalHeight(int rowCount) =>
BorderSize * 2 + OffsetSize * 2 + rowCount * EntryHeight + (rowCount - 1) * OffsetSize;
/// <summary>
/// Calculates content height given number of rows.
/// </summary>
public int GetContentHeight(int rowCount) => rowCount * EntryHeight + (rowCount - 1) * OffsetSize;
}

View file

@ -0,0 +1,162 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GridSizeSpec.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>
/// Specifies how a grid column or row should be sized.
/// Supports absolute pixels, percentage, and star (proportional) sizing.
/// </summary>
public readonly struct GridSizeSpec
{
/// <summary>
/// The sizing type.
/// </summary>
public readonly GridSizeType Type;
/// <summary>
/// The value - meaning depends on Type:
/// - Absolute: pixel size
/// - Percent: percentage of total size (1-100)
/// - Star: proportional weight (1 = equal share)
/// </summary>
public readonly int Value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private GridSizeSpec(GridSizeType type, int value)
{
Type = type;
Value = value;
}
/// <summary>
/// Creates an absolute pixel size specification.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridSizeSpec Absolute(int pixels) => new(GridSizeType.Absolute, pixels);
/// <summary>
/// Creates a star (proportional) size specification.
/// Star-sized tracks share the remaining space equally (after absolute/percent are allocated).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridSizeSpec Star() => new(GridSizeType.Star, 1);
/// <summary>
/// Creates a percentage size specification.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridSizeSpec Percent(int percent) => new(GridSizeType.Percent, percent);
/// <summary>
/// Parses a single size specification token.
/// Formats:
/// - "*" = star (equal share of remaining space)
/// - "10*" = 10% of total size
/// - "100" = 100 pixels absolute
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GridSizeSpec Parse(ReadOnlySpan<char> token)
{
token = token.Trim();
if (token.IsEmpty)
{
return Star();
}
// Check for star notation
var starIndex = token.IndexOf('*');
if (starIndex == -1)
{
// No star - absolute pixel value
return int.TryParse(token, out var pixels) ? Absolute(pixels) : Star();
}
if (starIndex == 0 && token.Length == 1)
{
// Just "*" - equal share star
return Star();
}
// "N*" format - percentage
var percentPart = token[..starIndex];
return int.TryParse(percentPart, out var percent) ? Percent(percent) : Star();
}
/// <summary>
/// Parses a space-separated size specification string into a span of GridSizeSpec values.
/// Returns the number of specs parsed.
/// </summary>
public static int ParseAll(ReadOnlySpan<char> sizeSpec, Span<GridSizeSpec> destination)
{
var count = 0;
var remaining = sizeSpec;
while (!remaining.IsEmpty && count < destination.Length)
{
// Skip leading whitespace
remaining = remaining.TrimStart();
if (remaining.IsEmpty)
{
break;
}
// Find end of current token
var spaceIndex = remaining.IndexOf(' ');
ReadOnlySpan<char> token;
if (spaceIndex < 0)
{
token = remaining;
remaining = default;
}
else
{
token = remaining[..spaceIndex];
remaining = remaining[(spaceIndex + 1)..];
}
destination[count++] = Parse(token);
}
return count;
}
}
/// <summary>
/// Specifies the type of grid size specification.
/// </summary>
public enum GridSizeType : byte
{
/// <summary>
/// Fixed pixel size.
/// </summary>
Absolute,
/// <summary>
/// Star sizing - shares remaining space equally with other star-sized tracks.
/// </summary>
Star,
/// <summary>
/// Percentage of total available size.
/// </summary>
Percent
}

View file

@ -0,0 +1,239 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ListViewLayout.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>
/// Calculates pagination and provides helper methods for a paginated list view.
/// This struct does NOT hold column data - column positions/widths are passed
/// separately via spans to avoid ref struct limitations.
/// </summary>
public ref struct ListViewLayout
{
private readonly int _originY;
private readonly int _headerHeight;
private readonly int _rowHeight;
/// <summary>
/// The total number of items in the list.
/// </summary>
public readonly int TotalItems;
/// <summary>
/// The number of items that can be displayed per page.
/// </summary>
public readonly int ItemsPerPage;
/// <summary>
/// The current page index (0-based).
/// </summary>
public readonly int CurrentPage;
/// <summary>
/// The total number of pages.
/// </summary>
public readonly int TotalPages;
/// <summary>
/// The index of the first item on the current page.
/// </summary>
public readonly int StartIndex;
/// <summary>
/// The number of items visible on the current page.
/// </summary>
public readonly int VisibleCount;
/// <summary>
/// Whether there is a previous page.
/// </summary>
public readonly bool CanGoBack;
/// <summary>
/// Whether there is a next page.
/// </summary>
public readonly bool CanGoNext;
/// <summary>
/// The number of columns in the list.
/// </summary>
public readonly int ColumnCount;
/// <summary>
/// The height of each data row.
/// </summary>
public int RowHeight
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _rowHeight;
}
/// <summary>
/// The height of the header row.
/// </summary>
public int HeaderHeight
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _headerHeight;
}
/// <summary>
/// Creates a new ListViewLayout and computes column positions into the provided spans.
/// </summary>
/// <param name="originX">The X origin of the list area.</param>
/// <param name="originY">The Y origin of the list area.</param>
/// <param name="width">The total width of the list area.</param>
/// <param name="height">The total height of the list area.</param>
/// <param name="totalItems">The total number of items in the data source.</param>
/// <param name="currentPage">The current page index (0-based).</param>
/// <param name="rowHeight">The height of each data row.</param>
/// <param name="headerHeight">The height of the header row (0 for no header).</param>
/// <param name="columnSpec">Space-separated column size specification.</param>
/// <param name="columnPositions">Span to receive calculated column X positions.</param>
/// <param name="columnWidths">Span to receive calculated column widths.</param>
/// <returns>The configured ListViewLayout.</returns>
public static ListViewLayout Create(
int originX,
int originY,
int width,
int height,
int totalItems,
int currentPage,
int rowHeight,
int headerHeight,
ReadOnlySpan<char> columnSpec,
Span<int> columnPositions,
Span<int> columnWidths
)
{
// Parse and compute column widths
var columnCount = GridCalculator.ComputeFromSpec(columnSpec, width, originX, columnPositions, columnWidths);
return new ListViewLayout(
originY,
headerHeight, rowHeight,
totalItems, currentPage,
height,
columnCount
);
}
private ListViewLayout(
int originY,
int headerHeight,
int rowHeight,
int totalItems,
int currentPage,
int totalHeight,
int columnCount
)
{
_originY = originY;
_headerHeight = headerHeight;
_rowHeight = rowHeight;
ColumnCount = columnCount;
TotalItems = totalItems;
// Calculate items per page based on available height
var contentHeight = totalHeight - headerHeight;
ItemsPerPage = contentHeight / rowHeight;
// Calculate pagination
if (totalItems > 0 && ItemsPerPage > 0)
{
TotalPages = (totalItems + ItemsPerPage - 1) / ItemsPerPage;
CurrentPage = Math.Clamp(currentPage, 0, TotalPages - 1);
StartIndex = CurrentPage * ItemsPerPage;
VisibleCount = Math.Min(ItemsPerPage, totalItems - StartIndex);
CanGoBack = CurrentPage > 0;
CanGoNext = CurrentPage < TotalPages - 1;
}
else
{
TotalPages = 1;
CurrentPage = 0;
StartIndex = 0;
VisibleCount = 0;
CanGoBack = false;
CanGoNext = false;
}
}
/// <summary>
/// Gets the header cell for the specified column using pre-computed column data.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell GetHeaderCell(int column, ReadOnlySpan<int> columnPositions, ReadOnlySpan<int> columnWidths) =>
new(columnPositions[column], _originY, columnWidths[column], _headerHeight);
/// <summary>
/// Gets the data cell for the specified visible row and column.
/// </summary>
/// <param name="visibleRowIndex">The 0-based index within visible rows (not data index).</param>
/// <param name="column">The column index.</param>
/// <param name="columnPositions">Pre-computed column X positions.</param>
/// <param name="columnWidths">Pre-computed column widths.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public GridCell GetRowCell(
int visibleRowIndex, int column, ReadOnlySpan<int> columnPositions, ReadOnlySpan<int> columnWidths
) => new(
columnPositions[column],
_originY + _headerHeight + visibleRowIndex * _rowHeight,
columnWidths[column],
_rowHeight
);
/// <summary>
/// Gets the data index for a visible row.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetDataIndex(int visibleRowIndex) => StartIndex + visibleRowIndex;
/// <summary>
/// Gets the Y position for a visible row.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetRowY(int visibleRowIndex) => _originY + _headerHeight + visibleRowIndex * _rowHeight;
/// <summary>
/// Gets the X position of a column.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColumnX(ReadOnlySpan<int> columnPositions, int column) => columnPositions[column];
/// <summary>
/// Gets the width of a column.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColumnWidth(ReadOnlySpan<int> columnWidths, int column) => columnWidths[column];
/// <summary>
/// Gets the horizontal center of a column.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColumnCenterX(ReadOnlySpan<int> columnPositions, ReadOnlySpan<int> columnWidths, int column) =>
columnPositions[column] + columnWidths[column] / 2;
/// <summary>
/// Gets the right edge of a column.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColumnRightX(ReadOnlySpan<int> columnPositions, ReadOnlySpan<int> columnWidths, int column) =>
columnPositions[column] + columnWidths[column];
}

View file

@ -0,0 +1,22 @@
namespace Server.Gumps;
public class GumpTextColors
{
public const string Gold = "#ffc959";
public const string White = "#ffffff";
public const string Blue = "#1D63DC";
public const string LightBlue = "#7799EE";
public const string Green = "#80E732";
public const string Orange = "#F28B00";
public const string Purple = "#F3ACF6";
public const string Red = "#B52B2D";
public const string BrightRed = "#FF0000";
public const string LightGreen = "#E6FFC0";
public const string Yellow = "#FFFFBB";
}
public class GumpHues
{
public const int Black = 0;
public const int White = 2049;
}

View file

@ -1,585 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpGrid.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.Collections.Generic;
using Server.Collections;
namespace Server.Gumps;
public class GumpGrid : Gump
{
private Dictionary<string, Grid> _grids = new();
public GumpGrid(int x, int y) : base(x, y)
{
}
private void AddColumn(string name, int x, int width)
{
_grids[name].Columns.Add(new Col { X = x, Width = width, });
}
private void AddRow(string name, int y, int height)
{
_grids[name].Rows.Add(new Row { Y = y, Height = height });
}
private void InitColumn(string name, int column, int w, int x)
{
var width = w / column;
for (var i = 0; i < column; i++)
{
AddColumn(name, i * width + x, width);
}
}
private void InitRow(string name, int row, int h, int y)
{
var height = h / row;
for (var i = 0; i < row; i++)
{
AddRow(name, i * height + y, height);
}
}
private static Swap Exist(ref PooledRefList<Swap> list, int index)
{
for (var i = 0; i < list.Count; i++)
{
var item = list[i];
if (item.Index == index)
{
return item;
}
}
return null;
}
private static int CalculateCustom(int column, int len, string[] sizes, ref PooledRefList<Swap> list)
{
var cropSize = 0;
for (var i = 0; i < column; i++)
{
if (sizes[i] == "*")
{
continue;
}
//percent
var percent = sizes[i].RemoveOrdinal("*");
var size = sizes[i] != percent ? len / 100 * int.Parse(percent) : int.Parse(percent);
list.Add(new Swap { Index = i, Size = size });
cropSize += size;
}
return cropSize;
}
private void InitCustomSizeRow(string name, int height, int row, string[] sizes, int y)
{
var list = PooledRefList<Swap>.Create();
var cropHeight = CalculateCustom(row, height, sizes, ref list);
var Height = height - cropHeight;
var factor = row - list.Count;
for (var i = 0; i < row; i++)
{
var swap = Exist(ref list, i);
if (swap != null)
{
var size = swap.Size;
if (i == 0)
{
AddRow(name, y, size);
}
else
{
var col = _grids[name].Rows[i - 1];
AddRow(name, col.Height + col.Y + y, size);
}
}
else
{
var size = Height / factor;
if (i == 0)
{
AddRow(name, y, size);
}
else
{
var col = _grids[name].Rows[i - 1];
AddRow(name, col.Height + col.Y + y, size);
}
}
}
list.Dispose();
}
private void InitCustomSizeColumn(string name, int width, int column, string[] sizes, int x)
{
var list = PooledRefList<Swap>.Create();
var cropWidth = CalculateCustom(column, width, sizes, ref list);
var Width = width - cropWidth;
var factor = column - list.Count;
for (var i = 0; i < column; i++)
{
var swap = Exist(ref list, i);
if (swap != null)
{
var size = swap.Size;
if (i == 0)
{
AddColumn(name, x, size);
}
else
{
var col = _grids[name].Columns[i - 1];
AddColumn(name, col.Width + col.X + x, size);
}
}
else
{
var size = Width / factor;
if (i == 0)
{
AddColumn(name, x, size);
}
else
{
var col = _grids[name].Columns[i - 1];
AddColumn(name, col.Width + col.X + x, size);
}
}
}
}
public Grid SubGrid(
string name,
string destGridName,
int columnStart,
int rowStart,
int createColumnsCount,
int createRowsCount,
int columnSpan = 0,
int rowSpan = 0,
string createColumnSize = "",
string createRowSize = "",
int marginX = 0,
int marginY = 0
)
{
if (CalculateCord(
destGridName,
columnStart,
rowStart,
columnSpan,
rowSpan,
out var x,
out var y,
out var width,
out var height
))
{
Grid(
name,
width,
height,
createColumnsCount,
createRowsCount,
createColumnSize,
createRowSize,
x + marginX,
y + marginY
);
}
return CreateGrid(name, 0, 0);
}
public Grid CreateGrid(string name, int width, int height)
{
if (!_grids.TryGetValue(name, out var grid))
{
grid = new Grid { Name = name, Width = width, Height = height };
_grids.Add(name, grid);
}
return grid;
}
public Grid Grid(
string name,
int width,
int height,
int columns,
int rows,
string columnSize = "",
string rowSize = "",
int x = 0,
int y = 0
)
{
if (columns < 1)
{
throw new Exception(nameof(columns));
}
if (rows < 1)
{
throw new Exception(nameof(columns));
}
var Grid = CreateGrid(name, width, height);
if (columnSize.Length > 0)
{
var buffer = columnSize.Split(' ');
if (buffer.Length == columns)
{
InitCustomSizeColumn(name, width, columns, buffer, x);
}
else
{
throw new Exception(nameof(columnSize));
}
}
else
{
InitColumn(name, columns, width, x);
}
if (rowSize.Length > 0)
{
var buffer = rowSize.Split(' ');
if (buffer.Length == rows)
{
InitCustomSizeRow(name, height, rows, buffer, y);
}
else
{
throw new Exception(nameof(rowSize));
}
}
else
{
InitRow(name, rows, height, y);
}
return Grid;
}
public bool CalculateCord(
string name,
int column,
int row,
int columnSpan,
int rowSpan,
out int x,
out int y,
out int width,
out int height
)
{
x = 0; y = 0; width = 0; height = 0;
if (_grids.TryGetValue(name, out var grid))
{
var count = columnSpan == 0 ? column + 1 : row + columnSpan;
for (var i = column; i < count; i++)
{
if (i >= grid.Columns.Count)
{
break;
}
var dcol = grid.Columns[i];
if (i == column)
{
x = dcol.X;
}
width += dcol.Width;
}
count = rowSpan == 0 ? row + 1 : row + rowSpan;
for (var i = row; i < count; i++)
{
if (i >= grid.Rows.Count)
{
break;
}
var drow = grid.Rows[i];
if (i == row)
{
y = drow.Y;
}
height += drow.Height;
}
if (width == 0)
{
width = grid.Width;
}
if (height == 0)
{
height = grid.Height;
}
return true;
}
return false;
}
public class ListView
{
public int LineCount { get; }
public int ItemsCount { get; }
public ListItem[] Items { get; }
public ListItem Header { get; }
public int Page { get; }
public int TotalPages { get; }
public int ColHeight { get; }
public bool CanNext { get; }
public bool CanBack { get; }
public int ColsCount { get; }
public ListView(
int itemsCount,
int columns,
int heightPerItem,
int page,
int x,
int y,
int height,
int pageItemCount,
int[] ColWidth,
int marginX = 0,
int marginY = 0,
int headerHeight = 0
)
{
ItemsCount = itemsCount;
var itemsPerPageCount = pageItemCount == 0 ? (height - marginY) / heightPerItem : pageItemCount;
LineCount = itemsPerPageCount;
if (itemsCount > 0)
{
TotalPages = itemsCount / itemsPerPageCount;
if (itemsCount % itemsPerPageCount == 0)
{
TotalPages--;
}
}
Page = page > TotalPages ? TotalPages : page;
ColsCount = columns;
ColHeight = heightPerItem;
var index = Page * itemsPerPageCount > 0 ? Page * itemsPerPageCount : 0;
var nowPageItems = index + itemsPerPageCount;
if (index > 0)
{
CanBack = true;
}
if (nowPageItems < itemsCount)
{
CanNext = true;
}
if (nowPageItems > itemsCount)
{
itemsPerPageCount = Page > 0 ? itemsCount - Page * itemsPerPageCount : itemsCount;
}
//header
Header = new ListItem
{
X = x,
Y = y + marginY,
Index = -1,
Cols = new Col[columns],
};
var length = x + marginX;
for (var col = 0; col < columns; col++)
{
Header.Cols[col] = new Col();
Header.Cols[col].Width = ColWidth[col];
Header.Cols[col].X = length;
length += ColWidth[col];
}
Items = new ListItem[itemsPerPageCount];
for (var i = 0; i < itemsPerPageCount; i++)
{
Items[i] = new ListItem
{
X = x,
Y = y + i * heightPerItem + marginY + headerHeight,
Index = index + i,
Cols = Header.Cols
};
}
}
}
public ListView AddListView(
string name,
int column,
int row,
int itemsCount,
int page,
int heightPerItem,
int createColumn,
int columnSpan = 0,
int rowSpan = 0,
int width = 0,
int height = 0,
int pageItemCount = 0,
int marginX = 0,
int marginY = 0,
int headerHeight = 0,
string colSize = ""
)
{
if (CalculateCord(name, column, row, columnSpan, rowSpan, out var x, out var y, out var w, out var h))
{
w = width > 0 ? width : w;
h = height > 0 ? height : h;
var colWidth = new int[createColumn];
if (colSize == string.Empty)
{
for (var i = 0; i < createColumn; i++)
{
colWidth[i] = w / createColumn;
}
}
else
{
var buffer = colSize.Split(' ');
if (buffer.Length > 0)
{
const string listName = "listView";
_grids.Add(listName, new Grid());
if (createColumn < buffer.Length)
{
Array.Resize(ref buffer, createColumn);
}
InitCustomSizeColumn(listName, w, createColumn, buffer, x);
var cols = _grids[listName].Columns;
for (var i = 0; i < createColumn; i++)
{
colWidth[i] = cols[i].Width;
}
_grids.Remove(listName);
}
}
return new ListView(
itemsCount,
createColumn,
heightPerItem,
page,
x,
y,
h,
pageItemCount,
colWidth,
marginX,
marginY,
headerHeight
);
}
return null;
}
}
public enum GridHues
{
White = 2049, // Change to 0x480 if you have old hues.mul
Black = 0,
}
public static class GridColors
{
public const string Gold = "#ffc959";
public const string White = "#ffffff";
public const string Blue = "#1D63DC";
public const string LightBlue = "#7799EE";
public const string Green = "#80E732";
public const string Orange = "#F28B00";
public const string Purple = "#F3ACF6";
public const string Red = "#B52B2D";
public const string LightGreen = "#E6FFC0";
public const string Yellow = "#FFFFBB";
}
public class ListItem
{
public int X { get; set; }
public int Y { get; set; }
public int Index { get; set; }
public Col[] Cols { get; set; }
}
public class Col
{
public int X { get; set; }
public int Width { get; set; }
public int HCenter => X + Width / 2;
public int HEnd => X + Width;
}
public class Row
{
public int Y { get; set; }
public int Height { get; set; }
public int VCenter => Y + Height / 2;
public int VEnd => Y + Height;
}
public class Grid
{
public string Name { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public List<Col> Columns { get; set; } = new();
public List<Row> Rows { get; set; } = new();
}
public class Swap
{
public int Index { get; set; }
public int Size { get; set; }
}