Fix tree rendering

Fixes some tree rendering problems where lines were not properly drawn
at some levels during some circumstances.

* Change the API back to only allow one root.
* Now uses a stack based approach to rendering instead of recursion.
* Removes the need for measuring the whole tree in advance.
  Leave this up to each child to render.
This commit is contained in:
Patrik Svensson
2021-01-09 18:34:07 +01:00
committed by Patrik Svensson
parent 0e0f4b4220
commit 8261b25e5c
34 changed files with 697 additions and 446 deletions

View File

@ -48,20 +48,20 @@ namespace Spectre.Console
{
var maxValue = Data.Max(item => item.Value);
var table = new Grid();
table.Collapse();
table.AddColumn(new GridColumn().PadRight(2).RightAligned());
table.AddColumn(new GridColumn().PadLeft(0));
table.Width = Width;
var grid = new Grid();
grid.Collapse();
grid.AddColumn(new GridColumn().PadRight(2).RightAligned());
grid.AddColumn(new GridColumn().PadLeft(0));
grid.Width = Width;
if (!string.IsNullOrWhiteSpace(Label))
{
table.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
}
foreach (var item in Data)
{
table.AddRow(
grid.AddRow(
new Markup(item.Label),
new ProgressBar()
{
@ -76,7 +76,7 @@ namespace Spectre.Console
});
}
return ((IRenderable)table).Render(context, maxWidth);
return ((IRenderable)grid).Render(context, maxWidth);
}
}
}

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
@ -7,155 +6,124 @@ using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Representation of tree data.
/// A renderable tree.
/// </summary>
public sealed class Tree : Renderable, IHasTreeNodes
{
private readonly TreeNode _root;
/// <summary>
/// Gets or sets the tree style.
/// </summary>
public Style Style { get; set; } = Style.Plain;
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the appearance of the tree.
/// Gets or sets the tree guide lines.
/// </summary>
public TreeAppearance Appearance { get; set; } = TreeAppearance.Ascii;
public TreeGuide Guide { get; set; } = TreeGuide.Line;
/// <summary>
/// Gets the tree nodes.
/// Gets the tree's child nodes.
/// </summary>
public List<TreeNode> Nodes { get; }
public List<Tree> Nodes { get; } = new List<Tree>();
/// <summary>
/// Gets or sets a value indicating whether or not the tree is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
/// <inheritdoc/>
List<TreeNode> IHasTreeNodes.Children => Nodes;
List<TreeNode> IHasTreeNodes.Nodes => _root.Nodes;
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
public Tree()
/// <param name="renderable">The tree label.</param>
public Tree(IRenderable renderable)
{
Nodes = new List<TreeNode>();
_root = new TreeNode(renderable);
}
/// <inheritdoc />
protected override Measurement Measure(RenderContext context, int maxWidth)
/// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class.
/// </summary>
/// <param name="label">The tree label.</param>
public Tree(string label)
{
Measurement MeasureAtDepth(RenderContext context, int maxWidth, TreeNode node, int depth)
{
var rootMeasurement = node.Measure(context, maxWidth);
var treeIndentation = depth * Appearance.PartSize;
var currentMax = rootMeasurement.Max + treeIndentation;
var currentMin = rootMeasurement.Min + treeIndentation;
foreach (var child in node.Children)
{
var childMeasurement = MeasureAtDepth(context, maxWidth, child, depth + 1);
if (childMeasurement.Min > currentMin)
{
currentMin = childMeasurement.Min;
}
if (childMeasurement.Max > currentMax)
{
currentMax = childMeasurement.Max;
}
}
return new Measurement(currentMin, Math.Min(currentMax, maxWidth));
}
if (Nodes.Count == 1)
{
return MeasureAtDepth(context, maxWidth, Nodes[0], depth: 0);
}
else
{
var root = new TreeNode(Text.Empty);
foreach (var node in Nodes)
{
root.AddNode(node);
}
return MeasureAtDepth(context, maxWidth, root, depth: 0);
}
_root = new TreeNode(new Markup(label));
}
/// <inheritdoc />
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (Nodes.Count == 1)
var result = new List<Segment>();
var stack = new Stack<Queue<TreeNode>>();
stack.Push(new Queue<TreeNode>(new[] { _root }));
var levels = new List<Segment>();
levels.Add(GetGuide(context, TreeGuidePart.Continue));
while (stack.Count > 0)
{
// Single root
return Nodes[0]
.Render(context, maxWidth)
.Concat(new List<Segment> { Segment.LineBreak })
.Concat(RenderChildren(context, maxWidth - Appearance.PartSize, Nodes[0], depth: 0));
}
else
{
// Multiple roots
var root = new TreeNode(Text.Empty);
foreach (var node in Nodes)
var stackNode = stack.Pop();
if (stackNode.Count == 0)
{
root.AddNode(node);
levels.RemoveLast();
if (levels.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.Fork));
}
continue;
}
return Enumerable.Empty<Segment>()
.Concat(RenderChildren(
context, maxWidth - Appearance.PartSize, root,
depth: 0));
}
}
var isLastChild = stackNode.Count == 1;
var current = stackNode.Dequeue();
private IEnumerable<Segment> RenderChildren(
RenderContext context, int maxWidth, TreeNode node,
int depth, int? trailingStarted = null)
{
var result = new List<Segment>();
foreach (var (_, _, lastChild, childNode) in node.Children.Enumerate())
{
var lines = Segment.SplitLines(context, childNode.Render(context, maxWidth));
foreach (var (_, isFirstLine, _, line) in lines.Enumerate())
stack.Push(stackNode);
if (isLastChild)
{
var siblingConnectorSegment =
new Segment(Appearance.GetPart(TreePart.SiblingConnector), Style);
if (trailingStarted != null)
{
result.AddRange(Enumerable.Repeat(siblingConnectorSegment, trailingStarted.Value));
result.AddRange(Enumerable.Repeat(
Segment.Padding(Appearance.PartSize),
depth - trailingStarted.Value));
}
else
{
result.AddRange(Enumerable.Repeat(siblingConnectorSegment, depth));
}
levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.End));
}
if (isFirstLine)
var prefix = levels.Skip(1).ToList();
var renderableLines = Segment.SplitLines(context, current.Renderable.Render(context, maxWidth - Segment.CellCount(context, prefix)));
foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate())
{
if (prefix.Count > 0)
{
result.Add(lastChild
? new Segment(Appearance.GetPart(TreePart.BottomChildBranch), Style)
: new Segment(Appearance.GetPart(TreePart.ChildBranch), Style));
}
else
{
result.Add(lastChild ? Segment.Padding(Appearance.PartSize) : siblingConnectorSegment);
result.AddRange(prefix.ToList());
}
result.AddRange(line);
result.Add(Segment.LineBreak);
if (isFirstLine && prefix.Count > 0)
{
var part = isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue;
prefix.AddOrReplaceLast(GetGuide(context, part));
}
}
var childTrailingStarted = trailingStarted ?? (lastChild ? depth : null);
if (current.Expanded && current.Nodes.Count > 0)
{
levels.AddOrReplaceLast(GetGuide(context, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue));
levels.Add(GetGuide(context, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork));
result.AddRange(
RenderChildren(
context, maxWidth - Appearance.PartSize,
childNode, depth + 1,
childTrailingStarted));
stack.Push(new Queue<TreeNode>(current.Nodes));
}
}
return result;
}
private Segment GetGuide(RenderContext context, TreeGuidePart part)
{
var guide = Guide.GetSafeTreeGuide(context.LegacyConsole || !context.Unicode);
return new Segment(guide.GetPart(part), Style ?? Style.Plain);
}
}
}

View File

@ -1,41 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Node of a tree.
/// Represents a tree node.
/// </summary>
public sealed class TreeNode : IHasTreeNodes, IRenderable
public sealed class TreeNode : IHasTreeNodes
{
private readonly IRenderable _renderable;
internal IRenderable Renderable { get; }
/// <inheritdoc/>
public List<TreeNode> Children { get; }
/// <summary>
/// Gets the tree node's child nodes.
/// </summary>
public List<TreeNode> Nodes { get; } = new List<TreeNode>();
/// <summary>
/// Gets or sets a value indicating whether or not the tree node is expanded or not.
/// </summary>
public bool Expanded { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="TreeNode"/> class.
/// </summary>
/// <param name="renderable">The <see cref="IRenderable"/> which this node wraps.</param>
/// <param name="children">Any children that the node is declared with.</param>
public TreeNode(IRenderable renderable, IEnumerable<TreeNode>? children = null)
/// <param name="renderable">The tree node label.</param>
public TreeNode(IRenderable renderable)
{
_renderable = renderable ?? throw new ArgumentNullException(nameof(renderable));
Children = new List<TreeNode>(children ?? Enumerable.Empty<TreeNode>());
}
/// <inheritdoc/>
public Measurement Measure(RenderContext context, int maxWidth)
{
return _renderable.Measure(context, maxWidth);
}
/// <inheritdoc/>
public IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return _renderable.Render(context, maxWidth);
Renderable = renderable;
}
}
}