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

Use LineCanvas to draw TableView borders #2574

Draft
wants to merge 32 commits into
base: v2_develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1d78048
Use LineCanvas to draw TableView borders
Nutzzz Apr 24, 2023
3aa57ec
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 3, 2023
cab5ca9
Fix merge
Nutzzz May 3, 2023
385f668
Fix column names
Nutzzz May 3, 2023
95a13cf
Fix most of the failing table unit tests
Nutzzz May 3, 2023
a1bbc6f
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 4, 2023
4f5bab7
Fix merge
Nutzzz May 4, 2023
62ed8a7
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 6, 2023
f036091
Fix missing separator after null cell entries where NullSymbol is nul…
Nutzzz May 6, 2023
8313dc0
Fix SeparatorSymbol (when cell lines are disabled)
Nutzzz May 6, 2023
f11a5d1
Change default for AlwaysUseNormalColorForVerticalCellLines back to f…
Nutzzz May 6, 2023
915dc36
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 10, 2023
9f23974
Fix merge
Nutzzz May 10, 2023
3a1d3ee
Add table LineStyles to TableEditor scenario
Nutzzz May 10, 2023
1898677
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 10, 2023
b64ea4f
Fix ExpandLastColumn=false and make HeaderThroughline respect LineStyle
Nutzzz May 10, 2023
29ce0e1
Merge branch 'tableview_linecanvasborders' of https://github.com/Nutz…
Nutzzz May 10, 2023
979c74d
Fix merge
Nutzzz May 10, 2023
846aab9
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 13, 2023
e701805
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz May 22, 2023
aeecd22
Merge branch 'v2_develop' into tableview_linecanvasborders
tig May 22, 2023
2baf075
Merge remote-tracking branch 'upstream/v2_develop' into tableview_lin…
Nutzzz May 25, 2023
ebf1c03
Update TableStyle.cs
Nutzzz May 25, 2023
503bc98
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz Oct 11, 2023
bbc4419
Merge branch 'v2_develop' into tableview_linecanvasborders
tig Jan 17, 2024
e8f8235
Merge branch 'v2_develop' into tableview_linecanvasborders
tig Jan 20, 2024
0c0d854
Fixes after upstream changes
Nutzzz Jan 21, 2024
3d37ad2
Merge branch 'v2_develop' into tableview_linecanvasborders
Nutzzz Jan 25, 2024
5a055f0
New features, including return of empty column behavior
Nutzzz Jan 29, 2024
a1c9808
Use Runes for SeparatorSymbols and BackgroundSymbol
Nutzzz Jan 29, 2024
b28d72e
Add glyph suggestion for SeparatorSymbols
Nutzzz Jan 29, 2024
d3c9a26
Allow user to use HeaderThroughline with HeaderOverline and HeaderUnd…
Nutzzz Feb 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 34 additions & 8 deletions Terminal.Gui/Views/TableView/TableStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class TableStyle {
/// <summary>
/// True to render a solid line through the headers (only when Overline and/or Underline are <see langword="false"/>)
/// </summary>
public bool ShowHorizontalHeaderThroughline { get; set; } = true;
public bool ShowHorizontalHeaderThroughline { get; set; } = false;

/// <summary>
/// True to render a solid line vertical line between cells
Expand All @@ -93,24 +93,39 @@ public class TableStyle {
/// </summary>
public bool ShowHorizontalBottomline { get; set; } = false;

/// <summary>
/// True to invert the colors of the entire selected cell in the <see cref="TableView"/>.
/// Helpful for when <see cref="TableView.FullRowSelect"/> is on, especially when the <see cref="ConsoleDriver"/> doesn't show
/// the cursor
/// </summary>
public bool InvertSelectedCell { get; set; } = false;

/// <summary>
/// True to invert the colors of the first symbol of the selected cell in the <see cref="TableView"/>.
/// This gives the appearance of a cursor for when the <see cref="ConsoleDriver"/> doesn't otherwise show
/// this
/// </summary>
public bool InvertSelectedCellFirstCharacter { get; set; } = false;

// TODO: Fix this, or just remove it [and SeparatorSymbol]?
// NOTE: This is equivalent to True by default after change to LineCanvas borders and can't be turned off
// without disabling ShowVerticalCellLines, however SeparatorSymbol and HeaderSeparatorSymbol could be
// used to approximate the previous default behavior with FullRowSelect
// TODO: Explore ways of changing this without a workaround
/// <summary>
/// Gets or sets a flag indicating whether to force <see cref="ColorScheme.Normal"/> use when rendering
/// vertical cell lines (even when <see cref="TableView.FullRowSelect"/> is on).
/// </summary>
//public bool AlwaysUseNormalColorForVerticalCellLines { get; set; } = false;

/// <summary>
/// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
/// The symbol to add after each header value to visually seperate values (if not using vertical gridlines)
/// </summary>
public char HeaderSeparatorSymbol { get; set; } = ' ';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our guidelines and just because it's a plain ol' good idea, how about defining constants for these instead of the char literal?

Fast way to do that would be to highlight the ' ' in one spot, press ctrl-alt-c, to invoke the resharper introduce constant refactoring, choose the option that says "replace x instances", which will cover any it thinks are relevant within the same scope, and then pick a name for the constant such as SingleSpace (or anything else that would make sense and also not be a potential conflict with any future strings or other types with a similar value/purpose).

And of course not necessarily in this PR. Just making note of a place for us to dogfood our guidelines.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if this is to be a constant it would make more sense as DefaultHeaderSeparatorSymbol rather than a global find and replace.

I think that approach would also work better with ConfigurationManager which has some precedent for changing defaults via settings files


/// <summary>
/// The symbol to add after each cell value to visually seperate values (if not using vertical gridlines)
/// </summary>
//public char SeparatorSymbol { get; set; } = ' ';
public char SeparatorSymbol { get; set; } = ' ';

/// <summary>
/// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
Expand All @@ -128,7 +143,8 @@ public class TableStyle {
public char CellPaddingSymbol { get; set; } = ' ';

/// <summary>
/// The symbol to pad outside table (if Style.ExpandLastColumn is false)
/// The symbol to pad outside table (if both <see cref="ExpandLastColumn"/> and <see cref="AddEmptyColumn"/>
/// are False)
/// </summary>
public char BackgroundSymbol { get; set; } = ' ';

Expand All @@ -144,15 +160,25 @@ public class TableStyle {
public RowColorGetterDelegate RowColorGetter { get; set; }

/// <summary>
/// Determines rendering when the last column in the table is visible but it's
/// Determines rendering when the last column in the table is visible but its
/// content or <see cref="ColumnStyle.MaxWidth"/> is less than the remaining
/// space in the control. True (the default) will expand the column to fill
/// the remaining bounds of the control. False will draw a column ending line
/// and leave a blank column that cannot be selected in the remaining space.
/// the remaining bounds of the control. If false, <see cref="AddEmptyColumn"/>
/// determines the behavior of the remaining space.
/// </summary>
/// <value></value>
public bool ExpandLastColumn { get; set; } = true;

/// <summary>
/// Determines rendering when the last column in the table is visible but its
/// content or <see cref="ColumnStyle.MaxWidth"/> is less than the remaining
/// space in the control *and* <see cref="ExpandLastColumn"/> is False. True (the default)
/// will add a blank column that cannot be selected in the remaining space.
/// False will fill the remaining space with <see cref="BackgroundSymbol"/>.
/// </summary>
/// <value></value>
public bool AddEmptyColumn { get; set; } = true;

/// <summary>
/// <para>
/// Determines how <see cref="TableView.ColumnOffset"/> is updated when scrolling
Expand Down
135 changes: 93 additions & 42 deletions Terminal.Gui/Views/TableView/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,30 +299,31 @@ public override void OnDrawContent (Rect contentArea)
bg = Style.BorderColor.Background;
}
Driver.SetAttribute (new Attribute(fg, bg));
RenderCellLines (width, Table.Rows, columnsToRender);

var lineWidth = width;

if (!Style.ExpandLastColumn && Style.AddEmptyColumn) {
lineWidth = contentArea.Width;
}
RenderCellLines (lineWidth, Table.Rows, columnsToRender);

foreach (var p in grid.GetMap (Bounds)) {
this.AddRune (p.Key.X, p.Key.Y, p.Value);
}

// render arrows
scrollRightPoint = null;
scrollLeftPoint = null;
int hh = GetHeaderHeightIfAny ();

// render arrows
if (Style.ShowHorizontalScrollIndicators) {
if (hh > 0 && MoreColumnsToLeft ()) {
scrollLeftPoint = new Point (0, hh);
AddRuneAt (Driver, 0, scrollLeftPoint.Value.Y - 1, CM.Glyphs.LeftArrow);
}
if (hh > 0 && MoreColumnsToRight (columnsToRender)) {
scrollRightPoint = new Point (width - 1, hh);
scrollRightPoint = new Point (lineWidth - 1, hh);
AddRuneAt (Driver, scrollRightPoint.Value.X, scrollRightPoint.Value.Y - 1, CM.Glyphs.RightArrow);
}
}
if (scrollLeftPoint != null) {
AddRuneAt (Driver, 0, scrollLeftPoint.Value.Y - 1, CM.Glyphs.LeftArrow);
}
if (scrollRightPoint != null) {
AddRuneAt (Driver, scrollRightPoint.Value.X, scrollRightPoint.Value.Y - 1, CM.Glyphs.RightArrow);
}

// render the header contents
if (Style.ShowHeaders && hh > 0) {
Expand All @@ -340,17 +341,15 @@ public override void OnDrawContent (Rect contentArea)
var colStyle = Style.GetColumnStyleIfAny (current.Column);
var colName = table.ColumnNames [current.Column];

/*
if (!Style.ShowVerticalHeaderLines && current.X - 1 >= 0) {
AddRune (current.X - 1, yh, (Rune)Style.SeparatorSymbol);
if (!Style.ShowVerticalHeaderLines && current.X > 0) {
AddRune (current.X - 1, yh, (Rune)Style.HeaderSeparatorSymbol);
}
*/

Move (current.X, yh);

if (current.Width - colName.Length > 0 &&
Style.ShowHorizontalHeaderThroughline &&
(!Style.ShowHorizontalHeaderOverline || !Style.ShowHorizontalHeaderUnderline)) {
if (current.Width - colName.Length > 0
Nutzzz marked this conversation as resolved.
Show resolved Hide resolved
&& Style.ShowHorizontalHeaderThroughline
&& (!Style.ShowHorizontalHeaderOverline || !Style.ShowHorizontalHeaderUnderline)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-optimization opportunity (also not in scope for this PR):

By DeMorgan's theorem, (!Style.ShowHorizontalHeaderOverline || !Style.ShowHorizontalHeaderUnderline) is equivalent to !(Style.ShowHorizontalHeaderOverline && Style.ShowHorizontalHeaderUnderline)

Each short-circuits if Style.ShowHorizontalHeaderOverline is false, but an extra unary ! is avoided with the && form if it is true, as there's only one in the latter case.

This is probably also applied by the compiler for optimized builds, though.

But a thought about that:

In places where debug and release builds may have different compiled output, it's a good idea to try to make them be the same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I don't need no stinkin' truth tables. I wasn't aware there was a difference in compiled output in this case. In fact, I guess I sort of always assumed that if there was, that a disjunction (with the potential for a "short-circuit") would be better, since (potentially) having to look up the additional value in memory would be much more expensive than doing the negation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha well as stated it is a micro-optimization and, in cases like that, my opinion is that clarity of intent is much more important than shaving off one operator method call in a subset of cases.

In this particular instance, they are identical except for the single situation I called out, so it's truly a super-micro-optimization.

But, for future reference, a lot of commentary I make, unless I put it in an actual change request review, are purely for visibility of opportunities, concepts, etc for discussion or for consideration for future work.

If I see an actual problem that I think should be addressed before merge, I'll be pretty clear about it and request a change in the review.

The only thing about the code in this PR that came close to that for me is that we strongly prefer switches (either expressions or statements as appropriate) for control flow logic, when possible, both for style and performance reasons.

But that's IMO not important enough, at this stage, to block a PR like this one. 😊

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and thanks for working on this.

Do you need any assistance, with regard to #2574 (comment) or anything else for this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Any assistance would be appreciated, and as I won't have time to work on this for the next few days there shouldn't be any duplication of effort.

Copy link
Author

@Nutzzz Nutzzz Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but only by dint of allowing users to make the strange choice of using all three lines.

Also, regarding switch-case, I don't use them unless there are at least 3 options [I assume that's standard?]; here there are a few 2-option if-else's and 2-option if-else-if's without a default else.

Of course that's not to say that some cleanup couldn't be done; this still retains much of the way the original version was structured.


if (colName.Sum (c => ((Rune)c).GetColumns ()) < current.Width) {
Driver.AddStr (colName);
Expand All @@ -361,20 +360,28 @@ public override void OnDrawContent (Rect contentArea)
Driver.AddStr (TruncateOrPad (colName, colName, current.Width, colStyle, padChar));
}

if (!Style.ShowVerticalHeaderLines
&& current.Column == columnsToRender.First().Column + columnsToRender.Length - 1
&& current.X + current.Width - 1 <= lineWidth) {
AddRune (current.X + current.Width - 1, yh, (Rune)Style.HeaderSeparatorSymbol);
}

if (!Style.ExpandLastColumn) {
/*
if (!Style.ShowVerticalHeaderLines && current.IsVeryLast) {
AddRune (current.X + current.Width - 1, yh, (Rune)Style.SeparatorSymbol);
}
*/
if (i == columnsToRender.Length - 1) {
for (int j = current.X + current.Width; j < Bounds.Width; j++) {
Driver.SetAttribute (GetNormalColor ());
AddRune (j, yh, (Rune)Style.BackgroundSymbol);
if (Style.ShowHorizontalHeaderUnderline) {
AddRune (j, yh + 1, (Rune)Style.BackgroundSymbol);
if (!Style.AddEmptyColumn) {
if (i == columnsToRender.Length - 1) {
for (int j = current.X + current.Width; j < Bounds.Width; j++) {
Driver.SetAttribute (GetNormalColor ());
AddRune (j, yh, (Rune)Style.BackgroundSymbol);
if (Style.ShowHorizontalHeaderOverline) {
AddRune (j, yh - 1, (Rune)Style.BackgroundSymbol);
}
if (Style.ShowHorizontalHeaderUnderline) {
AddRune (j, yh + 1, (Rune)Style.BackgroundSymbol);
}
}
}
} else if (!Style.ShowVerticalHeaderLines) {
AddRune (Bounds.Width - 1, yh, (Rune)Style.HeaderSeparatorSymbol);
}
}
}
Expand All @@ -394,11 +401,16 @@ public override void OnDrawContent (Rect contentArea)

// No more data
if (rowToRender >= Table.Rows) {
/*
if (rowToRender == Table.Rows.Count && Style.ShowHorizontalBottomline) {
RenderBottomLine (line, width, columnsToRender);
if (rowToRender == Table.Rows
&& Style.ShowHorizontalBottomline
&& !Style.ExpandLastColumn
&& !Style.AddEmptyColumn) {
var start = columnsToRender [^1];
for (int i = start.X + start.Width; i < Bounds.Width; i++) {
AddRune (i, Table.Rows + hh, (Rune)Style.BackgroundSymbol);
}
}
*/

continue;
}

Expand Down Expand Up @@ -468,7 +480,16 @@ private void RenderCellLines (int width, int height, ColumnToRender [] columnsTo
lineStyle = Style.OuterHeaderBorderStyle;
}
grid.AddLine (new Point (col.X - 1, 0), row, Orientation.Vertical, lineStyle);

// left side of empty column
if (col.Column == columnsToRender.First().Column + columnsToRender.Length - 1
&& !Style.ExpandLastColumn
&& Style.AddEmptyColumn) {
grid.AddLine (new Point (col.X + col.Width - 1, 0), row, Orientation.Vertical, lineStyle);
}
}

// right side
grid.AddLine (new Point (width - 1, 0), row, Orientation.Vertical, Style.OuterHeaderBorderStyle);
}
}
Expand All @@ -485,6 +506,13 @@ private void RenderCellLines (int width, int height, ColumnToRender [] columnsTo
lineStyle = Style.OuterBorderStyle;
}
grid.AddLine (new Point (col.X - 1, row - 1), height - RowOffset + 1, Orientation.Vertical, lineStyle);

// left side of empty column
if (col.Column == columnsToRender.First().Column + columnsToRender.Length - 1
&& !Style.ExpandLastColumn
&& Style.AddEmptyColumn) {
grid.AddLine (new Point (col.X + col.Width - 1, row - 1), height - RowOffset + 1, Orientation.Vertical, lineStyle);
}
}
grid.AddLine (new Point (width - 1, row - 1), height - RowOffset + 1, Orientation.Vertical, Style.OuterBorderStyle);
}
Expand Down Expand Up @@ -596,9 +624,23 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen
RenderCell (cellColor, render, isPrimaryCell);

// Style.AlwaysUseNormalColorForVerticalCellLines is no longer possible after switch to LineCanvas
// except when cell lines are disabled and Style.SeparatorSymbol is used
// except when vertical cell lines are disabled (though a Style.SeparatorSymbol could be used)

if (!Style.ShowVerticalCellLines) {
if (isSelectedCell && FullRowSelect) {
color = focused ? rowScheme.Focus : rowScheme.HotNormal;
} else {
color = Enabled ? rowScheme.Normal : rowScheme.Disabled;
}
Driver.SetAttribute (color);

if (current.X > 0) {
AddRune (current.X - 1, row, (Rune)Style.SeparatorSymbol);
}
if (current.X + current.Width - 1 < Bounds.Width) {
AddRune (current.X + current.Width - 1, row, (Rune)Style.SeparatorSymbol);
}

if (isSelectedCell) {
color = focused ? rowScheme.Focus : rowScheme.HotNormal;
} else {
Expand All @@ -607,10 +649,17 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen
Driver.SetAttribute (color);
}

if (!Style.ExpandLastColumn && i == columnsToRender.Length - 1) {
for (int j = current.X + current.Width; j < Bounds.Width; j++) {
Driver.SetAttribute (GetNormalColor ());
AddRune (j, row, (Rune)Style.BackgroundSymbol);
if (!Style.ExpandLastColumn) {
if (!Style.AddEmptyColumn) {
if (i == columnsToRender.Length - 1) {
for (int j = current.X + current.Width; j < Bounds.Width; j++) {
Driver.SetAttribute (GetNormalColor ());
AddRune (j, row, (Rune)Style.BackgroundSymbol);
}
}
} else if (!Style.ShowVerticalCellLines) {
Driver.SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled);
AddRune (Bounds.Width - 1, row, (Rune)Style.SeparatorSymbol);
}
}
}
Expand All @@ -631,16 +680,18 @@ protected virtual void RenderCell (Attribute cellColor, string render, bool isPr
// If the cell is the selected col/row then draw the first rune in inverted colors
// this allows the user to track which cell is the active one during a multi cell
// selection or in full row select mode
if (Style.InvertSelectedCellFirstCharacter && isPrimaryCell) {
if ((Style.InvertSelectedCell || Style.InvertSelectedCellFirstCharacter) && isPrimaryCell) {

if (render.Length > 0) {
// invert the color of the current cell for the first character
Driver.SetAttribute (new Attribute (cellColor.Background, cellColor.Foreground));
Driver.AddRune ((Rune)render [0]);

if (render.Length > 1) {
Driver.SetAttribute (cellColor);
Driver.AddStr (render.Substring (1));
if (!Style.InvertSelectedCell) {
Driver.SetAttribute (cellColor);
}
Driver.AddStr (render[1..]);
}
}
} else {
Expand Down Expand Up @@ -1728,7 +1779,7 @@ private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colSty
/// <returns></returns>
private string GetRepresentation (object value, ColumnStyle colStyle)
{
if (value == null || value == DBNull.Value) {
if (value is null || value == DBNull.Value) {
return string.IsNullOrEmpty(Style.NullSymbol) ? " " : Style.NullSymbol;
}

Expand Down