diff --git a/examples/Columns/Columns.csproj b/examples/Columns/Columns.csproj
new file mode 100644
index 0000000..04200cf
--- /dev/null
+++ b/examples/Columns/Columns.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Columns/Program.cs b/examples/Columns/Program.cs
new file mode 100644
index 0000000..654d93c
--- /dev/null
+++ b/examples/Columns/Program.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Spectre.Console;
+using Spectre.Console.Rendering;
+
+namespace ColumnsExample
+{
+ public static class Program
+ {
+ public static async Task Main()
+ {
+ // Download some random users
+ using var client = new HttpClient();
+ dynamic users = JObject.Parse(
+ await client.GetStringAsync("https://randomuser.me/api/?results=15"));
+
+ // Create a card for each user
+ var cards = new List();
+ foreach(var user in users.results)
+ {
+ cards.Add(new Panel(GetCard(user))
+ .SetHeader($"{user.location.country}")
+ .RoundedBorder().Expand());
+ }
+
+ // Render all cards in columns
+ AnsiConsole.Render(new Columns(cards));
+ }
+
+ private static string GetCard(dynamic user)
+ {
+ var name = $"{user.name.first} {user.name.last}";
+ var country = $"{user.location.city}";
+
+ return $"[b]{name}[/]\n[yellow]{country}[/]";
+ }
+ }
+}
diff --git a/examples/Diagnostic/Program.cs b/examples/Diagnostic/Program.cs
index c194bde..60cd796 100644
--- a/examples/Diagnostic/Program.cs
+++ b/examples/Diagnostic/Program.cs
@@ -1,4 +1,3 @@
-using System;
using Spectre.Console;
namespace Diagnostic
diff --git a/examples/Table/Program.cs b/examples/Table/Program.cs
index 36704b9..23dd678 100644
--- a/examples/Table/Program.cs
+++ b/examples/Table/Program.cs
@@ -1,5 +1,4 @@
using Spectre.Console;
-using Spectre.Console.Rendering;
namespace TableExample
{
diff --git a/src/.editorconfig b/src/.editorconfig
index c7304ba..31bd18c 100644
--- a/src/.editorconfig
+++ b/src/.editorconfig
@@ -82,5 +82,8 @@ dotnet_diagnostic.RCS1079.severity = warning
# RCS1057: Add empty line between declarations.
dotnet_diagnostic.RCS1057.severity = none
+# RCS1057: Validate arguments correctly
+dotnet_diagnostic.RCS1227.severity = none
+
# IDE0004: Remove Unnecessary Cast
dotnet_diagnostic.IDE0004.severity = warning
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/ColumnsTests.cs b/src/Spectre.Console.Tests/Unit/ColumnsTests.cs
new file mode 100644
index 0000000..9d6a455
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/ColumnsTests.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using Shouldly;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit
+{
+ public sealed class ColumnsTests
+ {
+ private sealed class User
+ {
+ public string Name { get; set; }
+ public string Country { get; set; }
+ }
+
+ [Fact]
+ public void Should_Render_Columns_Correctly()
+ {
+ // Given
+ var console = new PlainConsole(width: 61);
+ var users = new[]
+ {
+ new User { Name = "Savannah Thompson", Country = "Australia" },
+ new User { Name = "Sophie Ramos", Country = "United States" },
+ new User { Name = "Katrin Goldberg", Country = "Germany" },
+ };
+
+ var cards = new List();
+ foreach (var user in users)
+ {
+ cards.Add(
+ new Panel($"[b]{user.Name}[/]\n[yellow]{user.Country}[/]")
+ .RoundedBorder().Expand());
+ }
+
+ // When
+ console.Render(new Columns(cards));
+
+ // Then
+ console.Lines.Count.ShouldBe(4);
+ console.Lines[0].ShouldBe("╭────────────────────╮ ╭────────────────╮ ╭─────────────────╮");
+ console.Lines[1].ShouldBe("│ Savannah Thompson │ │ Sophie Ramos │ │ Katrin Goldberg │");
+ console.Lines[2].ShouldBe("│ Australia │ │ United States │ │ Germany │");
+ console.Lines[3].ShouldBe("╰────────────────────╯ ╰────────────────╯ ╰─────────────────╯");
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/GridTests.cs b/src/Spectre.Console.Tests/Unit/GridTests.cs
index 0003559..e873e5d 100644
--- a/src/Spectre.Console.Tests/Unit/GridTests.cs
+++ b/src/Spectre.Console.Tests/Unit/GridTests.cs
@@ -42,7 +42,7 @@ namespace Spectre.Console.Tests.Unit
}
[Fact]
- public void Should_Throw_If_Row_Columns_Is_Less_Than_Number_Of_Columns()
+ public void Should_Add_Empty_Items_If_User_Provides_Less_Row_Items_Than_Columns()
{
// Given
var grid = new Grid();
@@ -50,11 +50,10 @@ namespace Spectre.Console.Tests.Unit
grid.AddColumn();
// When
- var result = Record.Exception(() => grid.AddRow("Foo"));
+ grid.AddRow("Foo");
// Then
- result.ShouldBeOfType();
- result.Message.ShouldBe("The number of row columns are less than the number of grid columns.");
+ grid.RowCount.ShouldBe(1);
}
[Fact]
diff --git a/src/Spectre.Console.Tests/Unit/TableTests.cs b/src/Spectre.Console.Tests/Unit/TableTests.cs
index c58061a..ca0fde5 100644
--- a/src/Spectre.Console.Tests/Unit/TableTests.cs
+++ b/src/Spectre.Console.Tests/Unit/TableTests.cs
@@ -87,7 +87,7 @@ namespace Spectre.Console.Tests.Unit
}
[Fact]
- public void Should_Throw_If_Row_Columns_Is_Less_Than_Number_Of_Columns()
+ public void Should_Add_Empty_Items_If_User_Provides_Less_Row_Items_Than_Columns()
{
// Given
var table = new Table();
@@ -95,11 +95,10 @@ namespace Spectre.Console.Tests.Unit
table.AddColumn("World");
// When
- var result = Record.Exception(() => table.AddRow("Foo"));
+ table.AddRow("Foo");
// Then
- result.ShouldBeOfType();
- result.Message.ShouldBe("The number of row columns are less than the number of table columns.");
+ table.RowCount.ShouldBe(1);
}
[Fact]
diff --git a/src/Spectre.Console.Tests/Unit/TextTests.cs b/src/Spectre.Console.Tests/Unit/TextTests.cs
index 09f1141..e2121fd 100644
--- a/src/Spectre.Console.Tests/Unit/TextTests.cs
+++ b/src/Spectre.Console.Tests/Unit/TextTests.cs
@@ -65,6 +65,21 @@ namespace Spectre.Console.Tests.Unit
fixture.RawOutput.ShouldBe("Hello\n\nWorld\n\n");
}
+ [Fact]
+ public void Should_Render_Panel_2()
+ {
+ // Given
+ var console = new PlainConsole(width: 80);
+
+ // When
+ console.Render(new Markup("[b]Hello World[/]\n[yellow]Hello World[/]"));
+
+ // Then
+ console.Lines.Count.ShouldBe(2);
+ console.Lines[0].ShouldBe("Hello World");
+ console.Lines[1].ShouldBe("Hello World");
+ }
+
[Theory]
[InlineData(5, "Hello World", "Hello\nWorld")]
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]
diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln
index 04365e5..f4f4575 100644
--- a/src/Spectre.Console.sln
+++ b/src/Spectre.Console.sln
@@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors", "..\examples\Color
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostic", "..\examples\Diagnostic\Diagnostic.csproj", "{4337F255-88E9-4408-81A3-DF1AF58AC753}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Columns", "..\examples\Columns\Columns.csproj", "{33357599-C79D-4299-888F-634E2C3EACEF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -121,6 +123,18 @@ Global
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x64.Build.0 = Release|Any CPU
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x86.ActiveCfg = Release|Any CPU
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x86.Build.0 = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x64.Build.0 = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x86.Build.0 = Debug|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|x64.ActiveCfg = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|x64.Build.0 = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|x86.ActiveCfg = Release|Any CPU
+ {33357599-C79D-4299-888F-634E2C3EACEF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -131,6 +145,7 @@ Global
{C7FF6FDB-FB59-4517-8669-521C96AB7323} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{1F51C55C-BA4C-4856-9001-0F7924FFB179} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{4337F255-88E9-4408-81A3-DF1AF58AC753} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
+ {33357599-C79D-4299-888F-634E2C3EACEF} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
diff --git a/src/Spectre.Console/ConsoleExtensions.Rendering.cs b/src/Spectre.Console/ConsoleExtensions.Rendering.cs
index 096425d..b4e61dc 100644
--- a/src/Spectre.Console/ConsoleExtensions.Rendering.cs
+++ b/src/Spectre.Console/ConsoleExtensions.Rendering.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
@@ -30,8 +31,8 @@ namespace Spectre.Console
using (console.PushStyle(Style.Plain))
{
- var segments = renderable.Render(options, console.Width);
- segments = Segment.Merge(segments);
+ var segments = renderable.Render(options, console.Width).Where(x => !(x.Text.Length == 0 && !x.IsLineBreak)).ToArray();
+ segments = Segment.Merge(segments).ToArray();
var current = Style.Plain;
foreach (var segment in segments)
diff --git a/src/Spectre.Console/Rendering/Columns.cs b/src/Spectre.Console/Rendering/Columns.cs
new file mode 100644
index 0000000..ada2c1f
--- /dev/null
+++ b/src/Spectre.Console/Rendering/Columns.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// Renders things in columns.
+ ///
+ public sealed class Columns : Renderable, IPaddable, IExpandable
+ {
+ private readonly List _items;
+
+ ///
+ public Padding Padding { get; set; } = new Padding(0, 1);
+
+ ///
+ /// Gets or sets a value indicating whether or not the columns should
+ /// expand to the available space. If false, the column
+ /// width will be auto calculated. Defaults to true.
+ ///
+ public bool Expand { get; set; } = true;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The items to render.
+ public Columns(IEnumerable items)
+ {
+ if (items is null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ _items = new List(items);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The items to render.
+ public Columns(IEnumerable items)
+ {
+ if (items is null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ _items = new List(items.Select(item => new Markup(item)));
+ }
+
+ ///
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ var maxPadding = Math.Max(Padding.Left, Padding.Right);
+
+ var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
+ var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
+
+ var table = new Table();
+ table.NoBorder();
+ table.HideHeaders();
+ table.PadRightCell = false;
+
+ if (Expand)
+ {
+ table.Expand();
+ }
+
+ // Add columns
+ for (var index = 0; index < columnCount; index++)
+ {
+ table.AddColumn(new TableColumn(string.Empty)
+ {
+ Padding = Padding,
+ NoWrap = true,
+ });
+ }
+
+ // Add rows
+ for (var start = 0; start < _items.Count; start += columnCount)
+ {
+ table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
+ }
+
+ return ((IRenderable)table).Render(context, maxWidth);
+ }
+
+ // Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
+ private int CalculateColumnCount(int maxWidth, int[] itemWidths, int columnCount, int padding)
+ {
+ var widths = new Dictionary();
+ while (columnCount > 1)
+ {
+ var columnIndex = 0;
+ widths.Clear();
+
+ var exceededTotalWidth = false;
+ foreach (var renderableWidth in IterateWidths(itemWidths, columnCount))
+ {
+ widths[columnIndex] = Math.Max(widths.ContainsKey(columnIndex) ? widths[columnIndex] : 0, renderableWidth);
+ var totalWidth = widths.Values.Sum() + (padding * (widths.Count - 1));
+ if (totalWidth > maxWidth)
+ {
+ columnCount = widths.Count - 1;
+ exceededTotalWidth = true;
+ break;
+ }
+ else
+ {
+ columnIndex = (columnIndex + 1) % columnCount;
+ }
+ }
+
+ if (!exceededTotalWidth)
+ {
+ break;
+ }
+ }
+
+ return columnCount;
+ }
+
+ private IEnumerable IterateWidths(int[] itemWidths, int columnCount)
+ {
+ foreach (var width in itemWidths)
+ {
+ yield return width;
+ }
+
+ if (_items.Count % columnCount != 0)
+ {
+ for (var i = 0; i < columnCount - (_items.Count % columnCount) - 1; i++)
+ {
+ yield return 0;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Spectre.Console/Rendering/Grid.cs b/src/Spectre.Console/Rendering/Grid.cs
index 7a3a433..f261a26 100644
--- a/src/Spectre.Console/Rendering/Grid.cs
+++ b/src/Spectre.Console/Rendering/Grid.cs
@@ -7,7 +7,7 @@ namespace Spectre.Console
///
/// A renderable grid.
///
- public sealed class Grid : Renderable
+ public sealed class Grid : Renderable, IExpandable
{
private readonly Table _table;
@@ -21,6 +21,13 @@ namespace Spectre.Console
///
public int RowCount => _table.RowCount;
+ ///
+ public bool Expand
+ {
+ get => _table.Expand;
+ set => _table.Expand = value;
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -94,11 +101,6 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
- if (columns.Length < _table.ColumnCount)
- {
- throw new InvalidOperationException("The number of row columns are less than the number of grid columns.");
- }
-
if (columns.Length > _table.ColumnCount)
{
throw new InvalidOperationException("The number of row columns are greater than the number of grid columns.");
diff --git a/src/Spectre.Console/Rendering/Panel.cs b/src/Spectre.Console/Rendering/Panel.cs
index d83b1f9..2682e7d 100644
--- a/src/Spectre.Console/Rendering/Panel.cs
+++ b/src/Spectre.Console/Rendering/Panel.cs
@@ -64,8 +64,8 @@ namespace Spectre.Console
{
var childWidth = _child.Measure(context, maxWidth);
return new Measurement(
- childWidth.Min + 2 + Padding.GetHorizontalPadding(),
- childWidth.Max + 2 + Padding.GetHorizontalPadding());
+ childWidth.Min + EdgeWidth + Padding.GetHorizontalPadding(),
+ childWidth.Max + EdgeWidth + Padding.GetHorizontalPadding());
}
///
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 6afa7e5..f0a3562 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -13,10 +13,25 @@ namespace Spectre.Console.Rendering
[DebuggerDisplay("{Text,nq}")]
public class Segment
{
+ private readonly bool _mutable;
+ private string _text;
+
///
/// Gets the segment text.
///
- public string Text { get; internal set; }
+ public string Text
+ {
+ get => _text;
+ private set
+ {
+ if (!_mutable)
+ {
+ throw new NotSupportedException();
+ }
+
+ _text = value;
+ }
+ }
///
/// Gets a value indicating whether or not this is an expicit line break
@@ -39,12 +54,12 @@ namespace Spectre.Console.Rendering
///
/// Gets a segment representing a line break.
///
- public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true);
+ public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true, false);
///
/// Gets an empty segment.
///
- public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain);
+ public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false, false);
///
/// Initializes a new instance of the class.
@@ -65,14 +80,16 @@ namespace Spectre.Console.Rendering
{
}
- private Segment(string text, Style style, bool lineBreak)
+ private Segment(string text, Style style, bool lineBreak, bool mutable = true)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
- Text = text.NormalizeLineEndings();
+ _mutable = mutable;
+ _text = text.NormalizeLineEndings();
+
Style = style;
IsLineBreak = lineBreak;
IsWhiteSpace = string.IsNullOrWhiteSpace(text);
diff --git a/src/Spectre.Console/Rendering/Table.Calculations.cs b/src/Spectre.Console/Rendering/Table.Calculations.cs
index 87c1da4..d75ac7d 100644
--- a/src/Spectre.Console/Rendering/Table.Calculations.cs
+++ b/src/Spectre.Console/Rendering/Table.Calculations.cs
@@ -18,7 +18,7 @@ namespace Spectre.Console
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
private List CalculateColumnWidths(RenderContext options, int maxWidth)
{
- var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth));
+ var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth)).ToArray();
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
@@ -117,9 +117,17 @@ namespace Spectre.Console
private int GetExtraWidth(bool includePadding)
{
- var separators = _columns.Count - 1;
+ var hideBorder = BorderKind == BorderKind.None;
+ var separators = hideBorder ? 0 : _columns.Count - 1;
+ var edges = hideBorder ? 0 : EdgeCount;
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
- return separators + EdgeCount + padding;
+
+ if (!PadRightCell)
+ {
+ padding -= _columns.Last().Padding.Right;
+ }
+
+ return separators + edges + padding;
}
}
}
diff --git a/src/Spectre.Console/Rendering/Table.cs b/src/Spectre.Console/Rendering/Table.cs
index a1f47c7..5555d28 100644
--- a/src/Spectre.Console/Rendering/Table.cs
+++ b/src/Spectre.Console/Rendering/Table.cs
@@ -125,17 +125,19 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
- if (columns.Length < _columns.Count)
- {
- throw new InvalidOperationException("The number of row columns are less than the number of table columns.");
- }
-
if (columns.Length > _columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
_rows.Add(columns.ToList());
+
+ // Need to add missing columns?
+ if (columns.Length < _columns.Count)
+ {
+ var diff = _columns.Count - columns.Length;
+ Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
+ }
}
///
diff --git a/src/Spectre.Console/Rendering/Traits/Extensions/BorderExtensions.cs b/src/Spectre.Console/Rendering/Traits/Extensions/BorderExtensions.cs
index 55a8085..d66e7fa 100644
--- a/src/Spectre.Console/Rendering/Traits/Extensions/BorderExtensions.cs
+++ b/src/Spectre.Console/Rendering/Traits/Extensions/BorderExtensions.cs
@@ -1,6 +1,6 @@
using System;
-namespace Spectre.Console.Rendering
+namespace Spectre.Console
{
///
/// Contains extension methods for .
diff --git a/src/Spectre.Console/Rendering/Traits/Extensions/ExpandableExtensions.cs b/src/Spectre.Console/Rendering/Traits/Extensions/ExpandableExtensions.cs
index c565840..05f6880 100644
--- a/src/Spectre.Console/Rendering/Traits/Extensions/ExpandableExtensions.cs
+++ b/src/Spectre.Console/Rendering/Traits/Extensions/ExpandableExtensions.cs
@@ -1,6 +1,6 @@
using System;
-namespace Spectre.Console.Rendering
+namespace Spectre.Console
{
///
/// Contains extension methods for .