spectre.console
spectre.console copied to clipboard
Incorrect column width calculation for custom table borders
Information
- OS: Windows
- Version: 19044.1889
- Terminal: Windows Terminal
Describe the bug Column widths are not calculated correctly when one of the edges in a custom defined border have zero width.
To Reproduce
I'm attempting to reproduce the screen layout of a C program that I'm porting to C# and which uses ncurses
to generate its output. While portions of the UI could be considered tabular, it contains rows where its columns do not align with adjacent rows. For example, here's a small portion of the UI:
| AAAAAAAAAA AAAAAAAAAAA |
| DDDDDDD DDDDDDD |
| UUUUUUU | FFFFFF | SSSS |
| 1111 | 22 | 3 |
---------------------------
I don't know if there are better solutions, but my initial attempt has been to model something like this as a table with 1 column and 3 rows, where the third row is itself another table with 3 columns and 1 row. However, the UI is more complex than what I've shown in the example above as there are other adjacent elements and their borders need to be merged. So, I've defined a custom table border that can merge with similarly defined borders. For example:
public sealed class MergeTableBorder : TableBorder
{
private readonly bool _mergeLeft;
private readonly bool _mergeRight;
public MergeTableBorder(bool mergeLeft = false, bool mergeRight = false)
{
_mergeLeft = mergeLeft;
_mergeRight = mergeRight;
}
/// <inheritdoc/>
public override string GetPart(TableBorderPart part)
{
return part switch
{
TableBorderPart.HeaderTopLeft => _mergeLeft ? string.Empty : "┌",
TableBorderPart.HeaderTop => "─",
TableBorderPart.HeaderTopSeparator => "┬",
TableBorderPart.HeaderTopRight => _mergeRight ? "┬" : "┐",
TableBorderPart.HeaderLeft => _mergeLeft ? string.Empty : "│",
TableBorderPart.HeaderSeparator => "│",
TableBorderPart.HeaderRight => "│",
TableBorderPart.HeaderBottomLeft => _mergeLeft ? string.Empty : "├",
TableBorderPart.HeaderBottom => "─",
TableBorderPart.HeaderBottomSeparator => "┼",
TableBorderPart.HeaderBottomRight => _mergeRight ? "┼" : "┤",
TableBorderPart.CellLeft => _mergeLeft ? string.Empty : "│",
TableBorderPart.CellSeparator => "│",
TableBorderPart.CellRight => "│",
TableBorderPart.FooterTopLeft => _mergeLeft ? string.Empty : "├",
TableBorderPart.FooterTop => "─",
TableBorderPart.FooterTopSeparator => "┼",
TableBorderPart.FooterTopRight => _mergeRight ? "┼": "┤",
TableBorderPart.FooterBottomLeft => _mergeLeft ? string.Empty : "└",
TableBorderPart.FooterBottom => "─",
TableBorderPart.FooterBottomSeparator => "┴",
TableBorderPart.FooterBottomRight => _mergeRight ? "┴" : "┘",
_ => throw new InvalidOperationException("Unknown border part."),
};
}
}
However, it seems that the column widths are not calculated correctly when one of the edges in a border has zero width. To illustrate, here's sample code that defines a Table with three columns, each column containing an inner Table with a single column, and with the columns configured using the custom table border defined above so that the table edges are merged with adjacent elements.
AnsiConsole.Write(new Table()
.NoBorder()
.AddColumns(
new TableColumn(
new Table()
.Border(new MergeTableBorder(false, true))
.AddColumn("ABC")
.AddRow("123")).Padding(0, 0),
new TableColumn(
new Table()
.Border(new MergeTableBorder(true, true))
.AddColumn("DEF")
.AddRow("456")).Padding(0, 0),
new TableColumn(
new Table()
.Border(new MergeTableBorder(true, false))
.AddColumn("GHI")
.AddRow("789")).Padding(0, 0)));
Expected behavior The column borders in the adjacent tables should be contiguous.
Screenshots
I expect the following output:
Instead, the output contains unexpected whitespace to the right of the middle column.
Additional context
Tracing the cause, the problem seems to originate in the TableMeasurer
class, where it assumes that border edges always have non-zero widths.
https://github.com/spectreconsole/spectre.console/blob/506253bc3426e90f6c9052031a0e0e3814bebf0f/src/Spectre.Console/Widgets/Table/TableMeasurer.cs#L5
https://github.com/spectreconsole/spectre.console/blob/506253bc3426e90f6c9052031a0e0e3814bebf0f/src/Spectre.Console/Widgets/Table/TableMeasurer.cs#L39
I've only just started evaluating Spectre.Console so I have no idea whether this can break other scenarios, but I've successfully tested the following workaround that computes the left and right border edge widths instead of using a hard-coded value and which produces the correct output from the example above.
It consists in modifying the TableBorder class to include properties that return the width of the left and right edges.
public abstract partial class TableBorder
{
...
public int LeftBorderWidth => new int[]
{
GetPart(TableBorderPart.HeaderTopLeft).Length,
GetPart(TableBorderPart.HeaderLeft).Length,
GetPart(TableBorderPart.HeaderBottomLeft).Length,
GetPart(TableBorderPart.CellLeft).Length,
GetPart(TableBorderPart.FooterTopLeft).Length,
GetPart(TableBorderPart.FooterBottomLeft).Length,
}.Max();
public int RightBorderWidth => new int[]
{
GetPart(TableBorderPart.HeaderTopRight).Length,
GetPart(TableBorderPart.HeaderRight).Length,
GetPart(TableBorderPart.HeaderBottomRight).Length,
GetPart(TableBorderPart.CellRight).Length,
GetPart(TableBorderPart.FooterTopRight).Length,
GetPart(TableBorderPart.FooterBottomRight).Length,
}.Max();
...
}
The GetNonColumnWidth
method in the TableMeasurer
class can then be updated as shown below;
internal sealed class TableMeasurer : TableAccessor
{
// private const int EdgeCount = 2; // << DELETE THIS LINE
...
public int GetNonColumnWidth()
{
var hideBorder = !_border.Visible;
var separators = hideBorder ? 0 : Columns.Count - 1;
// var edges = hideBorder ? 0 : EdgeCount; // << REPLACE THIS LINE WITH THE LINE BELOW
var edges = hideBorder ? 0 : (LeftBorderWidth + RightBorderWidth);
var padding = Columns.Select(x => x.Padding?.GetWidth() ?? 0).Sum();
if (!_padRightCell)
{
padding -= Columns.Last().Padding.GetRightSafe();
}
return separators + edges + padding;
}
...
}
Please upvote :+1: this issue if you are interested in it.
@f2bo This is an excellent bug report! Would you be open to sending a PR to fix this?
My main concern is that I'm not really familiar with the library as I started evaluating it only a few days ago but I'll give it a shot.
I already have the fix mentioned above but I now see that there are other places where it is assumed that borders always have non-zero widths, for example, in the Panel
class. I've already fixed that too. However, I'm still seeing some unexpected whitespace in some other places that I need to investigate a bit more.
@f2bo I would say that each place can be fixed in separate PRs. Not all of them have to be fixed at once.
I would say that each place can be fixed in separate PRs. Not all of them have to be fixed at once.
@patriksvensson True, though I hoped to at least resolve the issues that are preventing me from creating the layout I wanted.
I have identified, and have potential fixes, for another two issues in addition to the one I describe above. Note that they all are the result of using a border where one or more of its edges has zero width. Given that they are all related, would you like me to include more details here and submit a single PR or would you prefer me to open issues and PRs for each problem separately?