Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add table/grid column width in fixed, sizeToConent (auto) and star (proportional) #707

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/input/widgets/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ table.HideHeaders();

```csharp
// Sets the table width to 50 cells
table.Width(50);
table.FixWidth(50);
```

### Alignment
Expand Down Expand Up @@ -135,5 +135,5 @@ table.Columns[0].NoWrap();

```csharp
// Set the column width
table.Columns[0].Width(15);
table.Columns[0].FixWidth(15);
```
2 changes: 1 addition & 1 deletion examples/Console/Showcase/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static void Main()
{
var table = new Table().HideHeaders().NoBorder();
table.Title("[u][yellow]Spectre.Console[/] [b]Features[/][/]");
table.AddColumn("Feature", c => c.NoWrap().RightAligned().Width(10).PadRight(3));
table.AddColumn("Feature", c => c.NoWrap().RightAligned().FixWidth(10).PadRight(3));
table.AddColumn("Demonstration", c => c.PadRight(0));
table.AddEmptyRow();

Expand Down
58 changes: 54 additions & 4 deletions src/Spectre.Console/Extensions/ColumnExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,71 @@ public static T NoWrap<T>(this T obj)
}

/// <summary>
/// Sets the width of the column.
/// Sets the width of the column to a fix size.
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IColumn"/>.</typeparam>
/// <param name="obj">The column.</param>
/// <param name="width">The column width.</param>
/// <param name="size">The column width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T Width<T>(this T obj, int? width)
public static T FixWidth<T>(this T obj, int size)
where T : class, IColumn
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}

obj.Width = width;
if (size < 0)
{
throw new ArgumentException("Fixed width cannot be negative", nameof(size));
}

obj.Width = size;
obj.SizeMode = SizeMode.Fixed;
return obj;
}

/// <summary>
/// Sets the width of the column to a proportional weight.
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IColumn"/>.</typeparam>
/// <param name="obj">The column.</param>
/// <param name="weight">The column width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T StarWidth<T>(this T obj, double weight = 1.0)
where T : class, IColumn
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}

if (weight < 0.0)
{
throw new ArgumentException("Weight cannot be negative", nameof(weight));
}

obj.Width = weight;
obj.SizeMode = SizeMode.Star;
return obj;
}

/// <summary>
/// Sets the column to auto size to content.
/// </summary>
/// <typeparam name="T">An object implementing <see cref="IColumn"/>.</typeparam>
/// <param name="obj">The column.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SizeToContent<T>(this T obj)
where T : class, IColumn
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}

obj.Width = null;
obj.SizeMode = SizeMode.SizeToContent;
return obj;
}
}
8 changes: 7 additions & 1 deletion src/Spectre.Console/IColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ public interface IColumn : IAlignable, IPaddable
/// <summary>
/// Gets or sets the width of the column.
/// </summary>
int? Width { get; set; }
double? Width { get; set; }

/// <summary>
/// Gets or sets the size mode, to define
/// how the <see cref="Width"/> value is interpreted.
/// </summary>
SizeMode SizeMode { get; set; }
}
16 changes: 16 additions & 0 deletions src/Spectre.Console/SizeMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Spectre.Console;

/// <summary>
/// Defines how the width value will be interpreted.
/// </summary>
public enum SizeMode
{
/// <summary>Fixed size.</summary>
Fixed = 1,

/// <summary>Proportional sizing.</summary>
Star = 2,

/// <summary>Auto-size width to content.</summary>
SizeToContent = 0,
}
36 changes: 33 additions & 3 deletions src/Spectre.Console/Widgets/GridColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace Spectre.Console;
public sealed class GridColumn : IColumn, IHasDirtyState
{
private bool _isDirty;
private int? _width;
private double? _width;
private SizeMode _sizeMode;
private bool _noWrap;
private Padding? _padding;
private Justify? _alignment;
Expand All @@ -16,14 +17,43 @@ public sealed class GridColumn : IColumn, IHasDirtyState

/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// The interpretation of width depends on the value of <see cref="SizeMode"/>.<br/>
/// <br/>
/// By default it is set to <see langword="null"/>, and the column will size to its contents <see cref="SizeMode.SizeToContent"/>
/// is set to <see cref="SizeMode.SizeToContent" />.
/// </summary>
public int? Width
public double? Width
{
get => _width;
set => MarkAsDirty(() => _width = value);
}

/// <summary>
/// Gets or sets the size mode which defines how the column width will be interpreted:
/// <list type="bullet">
/// <item>
/// <term><see cref="SizeMode.SizeToContent">SizeToContent (Auto)</see></term>
/// <description><see cref="Width" /> value is ignored and width will auto-size to content.</description>
/// </item>
/// <item>
/// <term><see cref="SizeMode.Fixed">Fixed</see></term>
/// <description><see cref="Width" /> value is interpreted as integer, fixed size.</description>
/// </item>
/// <item>
/// <term><see cref="SizeMode.Star">Star (*)</see></term>
/// <description><see cref="Width" /> value is interpreted as double and means proportional sizing. If the width value is <see langword="null"/> 1 is implied</description>
/// </item>
/// </list>
/// If mixed <see cref="SizeMode.SizeToContent" /> and <see cref="SizeMode.Fixed" /> widths with <see cref="SizeMode.Star" /> (proportional) widths:<br/>
/// The <see cref="SizeMode.Star" /> columns are apportioned to the remainder after the <see cref="SizeMode.SizeToContent" /> and
/// <see cref="SizeMode.Fixed" /> widths have been calculated.<br/>
/// </summary>
public SizeMode SizeMode
{
get => _sizeMode;
set => MarkAsDirty(() => _sizeMode = value);
}

/// <summary>
/// Gets or sets a value indicating whether wrapping of
/// text within the column should be prevented.
Expand Down
2 changes: 1 addition & 1 deletion src/Spectre.Console/Widgets/Table/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);

// Calculate the minimum and maximum table width
var measurements = _columns.Select(column => measurer.MeasureColumn(column, totalCellWidth));
var measurements = measurer.MeasureColumns(totalCellWidth);
var minTableWidth = measurements.Sum(x => x.Min) + measurer.GetNonColumnWidth();
var maxTableWidth = Width ?? measurements.Sum(x => x.Max) + measurer.GetNonColumnWidth();
return new Measurement(minTableWidth, maxTableWidth);
Expand Down
29 changes: 27 additions & 2 deletions src/Spectre.Console/Widgets/Table/TableColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,34 @@ public sealed class TableColumn : IColumn

/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to its contents.
/// The interpretation of width depends on the value of <see cref="SizeMode"/>.<br/>
/// <br/>
/// By default it is set to <see langword="null"/>, and the column will size to its contents <see cref="SizeMode.SizeToContent"/>
/// is set to <see cref="SizeMode.SizeToContent" />.
/// </summary>
public int? Width { get; set; }
public double? Width { get; set; }

/// <summary>
/// Gets or sets the size mode which defines how the column width will be interpreted:
/// <list type="bullet">
/// <item>
/// <term><see cref="SizeMode.SizeToContent">SizeToContent (Auto)</see></term>
/// <description><see cref="Width" /> value is ignored and width will auto-size to content.</description>
/// </item>
/// <item>
/// <term><see cref="SizeMode.Fixed">Fixed</see></term>
/// <description><see cref="Width" /> value is interpreted as integer, fixed size.</description>
/// </item>
/// <item>
/// <term><see cref="SizeMode.Star">Star (*)</see></term>
/// <description><see cref="Width" /> value is interpreted as double and means proportional sizing.</description>
/// </item>
/// </list>
/// If mixed <see cref="SizeMode.SizeToContent" /> and <see cref="SizeMode.Fixed" /> widths with <see cref="SizeMode.Star" /> (proportional) widths:<br/>
/// The <see cref="SizeMode.Star" /> columns are apportioned to the remainder after the <see cref="SizeMode.SizeToContent" /> and
/// <see cref="SizeMode.Fixed" /> widths have been calculated.<br/>
/// </summary>
public SizeMode SizeMode { get; set; }

/// <summary>
/// Gets or sets the padding of the column.
Expand Down
43 changes: 38 additions & 5 deletions src/Spectre.Console/Widgets/Table/TableMeasurer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public int GetNonColumnWidth()
/// <returns>A list of column widths.</returns>
public List<int> CalculateColumnWidths(int maxWidth)
{
var width_ranges = Columns.Select(column => MeasureColumn(column, maxWidth)).ToArray();
var width_ranges = MeasureColumns(maxWidth);
var widths = width_ranges.Select(range => range.Max).ToList();

var tableWidth = widths.Sum();
Expand All @@ -68,7 +68,7 @@ public List<int> CalculateColumnWidths(int maxWidth)
if (tableWidth > maxWidth)
{
var excessWidth = tableWidth - maxWidth;
widths = Ratio.Reduce(excessWidth, widths.Select(_ => 1).ToList(), widths, widths);
widths = Ratio.Reduce(excessWidth, widths.ConvertAll(_ => 1), widths, widths);
tableWidth = widths.Sum();
}
}
Expand All @@ -82,12 +82,45 @@ public List<int> CalculateColumnWidths(int maxWidth)
return widths;
}

public Measurement MeasureColumn(TableColumn column, int maxWidth)
internal Measurement[] MeasureColumns(int maxWidth)
{
IEnumerable<(int Index, Measurement Measurement)> MeasureStarColumns(IEnumerable<(TableColumn Column, int Index)> starColumns, int totalWidthForStar)
{
var sumStarWeight = starColumns.Sum(x => x.Column.Width ?? 1);
var ratio = totalWidthForStar / sumStarWeight;
foreach (var x in starColumns)
{
var starWidth = (int)Math.Round((x.Column.Width ?? 1) * ratio);

yield return (x.Index, new Measurement(starWidth, starWidth));
}
}

// index and separate columns
var indexColumns = Columns.Select((column, index) => (column, index)).ToList();
var fixedColumns = indexColumns.Where(x => x.column.SizeMode != SizeMode.Star);
var starColumns = indexColumns.Where(x => x.column.SizeMode == SizeMode.Star);

// first calculate fixed cells
var fixedWidth_ranges = fixedColumns.Select(x => (x.index, measurement: MeasureColumn(x.column, maxWidth))).ToList();

// get remainder
var consumedWidth = fixedWidth_ranges.Sum(x => x.measurement.Max);
var totalWidthForStar = Math.Max(0, maxWidth - consumedWidth);

// and apportioned to the remainder
var starWidth_ranges = MeasureStarColumns(starColumns, totalWidthForStar);
var width_ranges = fixedWidth_ranges.Concat(starWidth_ranges).OrderBy(x => x.Item1).Select(x => x.Item2);

return width_ranges.ToArray();
}

private Measurement MeasureColumn(TableColumn column, int maxWidth)
{
// Predetermined width?
if (column.Width != null)
if (column.Width != null && column.SizeMode == SizeMode.Fixed)
{
return new Measurement(column.Width.Value, column.Width.Value);
return new Measurement((int)column.Width.Value, (int)column.Width.Value);
}

var columnIndex = Columns.IndexOf(column);
Expand Down