Streamline tree API a bit

This commit is contained in:
Patrik Svensson 2021-01-02 10:47:29 +01:00 committed by Patrik Svensson
parent b136d0299b
commit 4bfb24bfcb
13 changed files with 245 additions and 228 deletions

View File

@ -1,90 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public class TreeMeasureTests
{
[Fact]
public void Measure_Tree_Dominated_Width()
{
// Given
var nestedChildren =
Enumerable.Range(0, 10)
.Select(x => new TreeNode(new Text($"multiple \n line {x}")));
var child3 = new TreeNode(new Text("child3"));
child3.AddChild(new TreeNode(new Text("single leaf\n multiline")));
var children = new List<TreeNode>
{
new(new Text("child1"), nestedChildren), new(new Text("child2")), child3,
};
var root = new TreeNode(new Text("Root node"), children);
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
// Corresponds to "│ └── multiple"
Assert.Equal(17, measurement.Min);
// Corresponds to " └── single leaf" when untrimmed
Assert.Equal(19, measurement.Max);
}
[Fact]
public void Measure_Max_Width_Bound()
{
// Given
var root = new TreeNode(new Text("Root node"));
var currentNode = root;
foreach (var i in Enumerable.Range(0, 100))
{
var newNode = new TreeNode(new Text(string.Empty));
currentNode.AddChild(newNode);
currentNode = newNode;
}
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
// Each node depth contributes 4 characters, so 100 node depth -> 400 character min width
Assert.Equal(400, measurement.Min);
// Successfully capped at 80 terminal width
Assert.Equal(80, measurement.Max);
}
[Fact]
public void Measure_Leaf_Dominated_Width()
{
// Given
var root = new TreeNode(new Text("Root node"));
var currentNode = root;
foreach (var i in Enumerable.Range(0, 10))
{
var newNode = new TreeNode(new Text(string.Empty));
currentNode.AddChild(newNode);
currentNode = newNode;
}
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
// Corresponds to "│ │ │ │ │ │ │ │ │ └── "
Assert.Equal(40, measurement.Min);
// Corresponds to "│ │ │ │ │ │ │ │ │ └── "
Assert.Equal(40, measurement.Max);
}
}
}

View File

@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Spectre.Console.Testing;
using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
[UsesVerify]
public sealed class TreeRenderingTests
{
[Fact]
public Task Representative_Tree()
{
// Given
var console = new FakeConsole(width: 80);
var nestedChildren =
Enumerable.Range(0, 10)
.Select(x => new TreeNode(new Text($"multiple \n line {x}")));
var child2 = new TreeNode(new Text("child2"));
var child2Child = new TreeNode(new Text("child2Child"));
child2.AddChild(child2Child);
child2Child.AddChild(new TreeNode(new Text("Child 2 child\n child")));
var child3 = new TreeNode(new Text("child3"));
var child3Child = new TreeNode(new Text("single leaf\n multiline"));
child3Child.AddChild(new TreeNode(new Calendar(2020, 01)));
child3.AddChild(child3Child);
var children = new List<TreeNode> { new(new Text("child1"), nestedChildren), child2, child3 };
var root = new TreeNode(new Text("Root node"), children);
var tree = new Tree(root);
// When
console.Render(tree);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
public Task Root_Node_Only()
{
// Given
var console = new FakeConsole(width: 80);
var root = new TreeNode(new Text("Root node"), Enumerable.Empty<TreeNode>());
var tree = new Tree(root);
// When
console.Render(tree);
// Then
return Verifier.Verify(console.Output);
}
}
}

View File

@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
using Spectre.Console.Rendering;
using Spectre.Console.Testing;
using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
[UsesVerify]
public class TreeTests
{
[Fact]
public void Should_Measure_Tree_Correctly()
{
// Given
var nestedChildren =
Enumerable.Range(0, 10)
.Select(x => new TreeNode(new Text($"multiple \n line {x}")));
var child3 = new TreeNode(new Text("child3"));
child3.AddChild(new TreeNode(new Text("single leaf\n multiline")));
var children = new List<TreeNode>
{
new(new Text("child1"), nestedChildren), new(new Text("child2")), child3,
};
var root = new TreeNode(new Text("Root node"), children);
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
measurement.Min.ShouldBe(17);
measurement.Max.ShouldBe(19);
}
[Fact]
public void Should_Measure_Tree_Correctly_With_Regard_To_Max_Width()
{
// Given
var root = new TreeNode(new Text("Root node"));
var currentNode = root;
foreach (var i in Enumerable.Range(0, 100))
{
var newNode = new TreeNode(new Text(string.Empty));
currentNode.AddChild(newNode);
currentNode = newNode;
}
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
measurement.Min.ShouldBe(400);
measurement.Max.ShouldBe(80);
}
[Fact]
public void Measure_Leaf_Dominated_Width()
{
// Given
var root = new TreeNode(new Text("Root node"));
var currentNode = root;
foreach (var i in Enumerable.Range(0, 10))
{
var newNode = new TreeNode(new Text(i.ToString()));
currentNode.AddChild(newNode);
currentNode = newNode;
}
var tree = new Tree(root);
// When
var measurement = ((IRenderable)tree).Measure(new RenderContext(Encoding.Unicode, false), 80);
// Then
measurement.Min.ShouldBe(41);
measurement.Max.ShouldBe(41);
}
[Fact]
public Task Should_Render_Tree_Correctly()
{
// Given
var console = new FakeConsole(width: 80);
var nestedChildren =
Enumerable.Range(0, 10)
.Select(x => new TreeNode(new Text($"multiple \n line {x}")));
var child2 = new TreeNode(new Text("child2"));
var child2Child = new TreeNode(new Text("child2Child"));
child2.AddChild(child2Child);
child2Child.AddChild(new TreeNode(new Text("Child 2 child\n child")));
var child3 = new TreeNode(new Text("child3"));
var child3Child = new TreeNode(new Text("single leaf\n multiline"));
child3Child.AddChild(new TreeNode(new Calendar(2020, 01)));
child3.AddChild(child3Child);
var children = new List<TreeNode> { new(new Text("child1"), nestedChildren), child2, child3 };
var root = new TreeNode(new Text("Root node"), children);
var tree = new Tree(root);
// When
console.Render(tree);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
public Task Should_Render_Tree_With_Only_Root_Node_Correctly()
{
// Given
var console = new FakeConsole(width: 80);
var root = new TreeNode(new Text("Root node"), Enumerable.Empty<TreeNode>());
var tree = new Tree(root);
// When
console.Render(tree);
// Then
return Verifier.Verify(console.Output);
}
}
}

View File

@ -0,0 +1,27 @@
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IHasCulture"/>.
/// </summary>
public static class HasTreeNodeExtensions
{
/// <summary>
/// Adds a child to the end of the node's list of children.
/// </summary>
/// <typeparam name="T">An object type with tree nodes.</typeparam>
/// <param name="obj">The object that has tree nodes.</param>
/// <param name="child">Child to be added.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T AddChild<T>(this T obj, TreeNode child)
where T : class, IHasTreeNodes
{
if (obj is null)
{
throw new System.ArgumentNullException(nameof(obj));
}
obj.Children.Add(child);
return obj;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Spectre.Console
{
/// <summary>
/// Represents something that has tree nodes.
/// </summary>
public interface IHasTreeNodes
{
/// <summary>
/// Gets the children of this node.
/// </summary>
public List<TreeNode> Children { get; }
}
}

View File

@ -5,10 +5,13 @@ namespace Spectre.Console.Rendering
/// <summary> /// <summary>
/// An ASCII rendering of a tree. /// An ASCII rendering of a tree.
/// </summary> /// </summary>
public sealed class AsciiTreeRendering : ITreeRendering public sealed class AsciiTreeAppearance : TreeAppearance
{ {
/// <inheritdoc/> /// <inheritdoc/>
public string GetPart(TreePart part) public override int PartSize => 4;
/// <inheritdoc/>
public override string GetPart(TreePart part)
{ {
return part switch return part switch
{ {
@ -18,8 +21,5 @@ namespace Spectre.Console.Rendering
_ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."),
}; };
} }
/// <inheritdoc/>
public int PartSize => 4;
} }
} }

View File

@ -1,13 +0,0 @@
namespace Spectre.Console.Rendering
{
/// <summary>
/// Selection of different renderings which can be used by <see cref="Tree"/>.
/// </summary>
public static class TreeRendering
{
/// <summary>
/// Gets ASCII rendering of a tree.
/// </summary>
public static ITreeRendering Ascii { get; } = new AsciiTreeRendering();
}
}

View File

@ -0,0 +1,15 @@
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a tree appearance.
/// </summary>
public abstract partial class TreeAppearance
{
/// <summary>
/// Gets ASCII rendering of a tree.
/// </summary>
public static TreeAppearance Ascii { get; } = new AsciiTreeAppearance();
}
}

View File

@ -1,20 +1,22 @@
namespace Spectre.Console.Rendering using Spectre.Console.Rendering;
namespace Spectre.Console
{ {
/// <summary> /// <summary>
/// Represents the characters used to render a tree. /// Represents a tree appearance.
/// </summary> /// </summary>
public interface ITreeRendering public abstract partial class TreeAppearance
{ {
/// <summary>
/// Gets the length of all tree part strings.
/// </summary>
public abstract int PartSize { get; }
/// <summary> /// <summary>
/// Get the set of characters used to render the corresponding <see cref="TreePart"/>. /// Get the set of characters used to render the corresponding <see cref="TreePart"/>.
/// </summary> /// </summary>
/// <param name="part">The part of the tree to get rendering string for.</param> /// <param name="part">The part of the tree to get rendering string for.</param>
/// <returns>Rendering string for the tree part.</returns> /// <returns>Rendering string for the tree part.</returns>
string GetPart(TreePart part); public abstract string GetPart(TreePart part);
/// <summary>
/// Gets the length of all tree part strings.
/// </summary>
int PartSize { get; }
} }
} }

View File

@ -19,9 +19,9 @@ namespace Spectre.Console
public Style Style { get; set; } = Style.Plain; public Style Style { get; set; } = Style.Plain;
/// <summary> /// <summary>
/// Gets or sets the rendering type used for the tree. /// Gets or sets the appearance of the tree.
/// </summary> /// </summary>
public ITreeRendering Rendering { get; set; } = TreeRendering.Ascii; public TreeAppearance Appearance { get; set; } = TreeAppearance.Ascii;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Tree"/> class. /// Initializes a new instance of the <see cref="Tree"/> class.
@ -29,19 +29,16 @@ namespace Spectre.Console
/// <param name="rootNode">Root node of the tree to be rendered.</param> /// <param name="rootNode">Root node of the tree to be rendered.</param>
public Tree(TreeNode rootNode) public Tree(TreeNode rootNode)
{ {
_rootNode = rootNode; _rootNode = rootNode ?? throw new ArgumentNullException(nameof(rootNode));
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Measurement Measure(RenderContext context, int maxWidth) protected override Measurement Measure(RenderContext context, int maxWidth)
{ {
return MeasureAtDepth(context, maxWidth, _rootNode, depth: 0); Measurement MeasureAtDepth(RenderContext context, int maxWidth, TreeNode node, int depth)
}
private Measurement MeasureAtDepth(RenderContext context, int maxWidth, TreeNode node, int depth)
{ {
var rootMeasurement = node.Measure(context, maxWidth); var rootMeasurement = node.Measure(context, maxWidth);
var treeIndentation = depth * Rendering.PartSize; var treeIndentation = depth * Appearance.PartSize;
var currentMax = rootMeasurement.Max + treeIndentation; var currentMax = rootMeasurement.Max + treeIndentation;
var currentMin = rootMeasurement.Min + treeIndentation; var currentMin = rootMeasurement.Min + treeIndentation;
@ -62,33 +59,34 @@ namespace Spectre.Console
return new Measurement(currentMin, Math.Min(currentMax, maxWidth)); return new Measurement(currentMin, Math.Min(currentMax, maxWidth));
} }
return MeasureAtDepth(context, maxWidth, _rootNode, depth: 0);
}
/// <inheritdoc /> /// <inheritdoc />
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{ {
return _rootNode return _rootNode
.Render(context, maxWidth) .Render(context, maxWidth)
.Concat(new List<Segment> { Segment.LineBreak }) .Concat(new List<Segment> { Segment.LineBreak })
.Concat(RenderChildren(context, maxWidth - Rendering.PartSize, _rootNode, depth: 0)); .Concat(RenderChildren(context, maxWidth - Appearance.PartSize, _rootNode, depth: 0));
} }
private IEnumerable<Segment> RenderChildren(RenderContext context, int maxWidth, TreeNode node, int depth, private IEnumerable<Segment> RenderChildren(RenderContext context, int maxWidth, TreeNode node, int depth,
int? trailingStarted = null) int? trailingStarted = null)
{ {
var result = new List<Segment>(); var result = new List<Segment>();
foreach (var (_, _, lastChild, childNode) in node.Children.Enumerate())
foreach (var (index, firstChild, lastChild, childNode) in node.Children.Enumerate())
{ {
var lines = Segment.SplitLines(context, childNode.Render(context, maxWidth)); var lines = Segment.SplitLines(context, childNode.Render(context, maxWidth));
foreach (var (_, isFirstLine, _, line) in lines.Enumerate())
foreach (var (lineIndex, firstLine, lastLine, line) in lines.Enumerate())
{ {
var siblingConnectorSegment = var siblingConnectorSegment =
new Segment(Rendering.GetPart(TreePart.SiblingConnector), Style); new Segment(Appearance.GetPart(TreePart.SiblingConnector), Style);
if (trailingStarted != null) if (trailingStarted != null)
{ {
result.AddRange(Enumerable.Repeat(siblingConnectorSegment, trailingStarted.Value)); result.AddRange(Enumerable.Repeat(siblingConnectorSegment, trailingStarted.Value));
result.AddRange(Enumerable.Repeat( result.AddRange(Enumerable.Repeat(
Segment.Padding(Rendering.PartSize), Segment.Padding(Appearance.PartSize),
depth - trailingStarted.Value)); depth - trailingStarted.Value));
} }
else else
@ -96,15 +94,15 @@ namespace Spectre.Console
result.AddRange(Enumerable.Repeat(siblingConnectorSegment, depth)); result.AddRange(Enumerable.Repeat(siblingConnectorSegment, depth));
} }
if (firstLine) if (isFirstLine)
{ {
result.Add(lastChild result.Add(lastChild
? new Segment(Rendering.GetPart(TreePart.BottomChildBranch), Style) ? new Segment(Appearance.GetPart(TreePart.BottomChildBranch), Style)
: new Segment(Rendering.GetPart(TreePart.ChildBranch), Style)); : new Segment(Appearance.GetPart(TreePart.ChildBranch), Style));
} }
else else
{ {
result.Add(lastChild ? Segment.Padding(Rendering.PartSize) : siblingConnectorSegment); result.Add(lastChild ? Segment.Padding(Appearance.PartSize) : siblingConnectorSegment);
} }
result.AddRange(line); result.AddRange(line);
@ -112,7 +110,11 @@ namespace Spectre.Console
} }
var childTrailingStarted = trailingStarted ?? (lastChild ? depth : null); var childTrailingStarted = trailingStarted ?? (lastChild ? depth : null);
result.AddRange(RenderChildren(context, maxWidth - Rendering.PartSize, childNode, depth + 1,
result.AddRange(
RenderChildren(
context, maxWidth - Appearance.PartSize,
childNode, depth + 1,
childTrailingStarted)); childTrailingStarted));
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
@ -7,10 +8,12 @@ namespace Spectre.Console
/// <summary> /// <summary>
/// Node of a tree. /// Node of a tree.
/// </summary> /// </summary>
public sealed class TreeNode : IRenderable public sealed class TreeNode : IHasTreeNodes, IRenderable
{ {
private readonly IRenderable _renderable; private readonly IRenderable _renderable;
private List<TreeNode> _children;
/// <inheritdoc/>
public List<TreeNode> Children { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TreeNode"/> class. /// Initializes a new instance of the <see cref="TreeNode"/> class.
@ -19,25 +22,8 @@ namespace Spectre.Console
/// <param name="children">Any children that the node is declared with.</param> /// <param name="children">Any children that the node is declared with.</param>
public TreeNode(IRenderable renderable, IEnumerable<TreeNode>? children = null) public TreeNode(IRenderable renderable, IEnumerable<TreeNode>? children = null)
{ {
_renderable = renderable; _renderable = renderable ?? throw new ArgumentNullException(nameof(renderable));
_children = new List<TreeNode>(children ?? Enumerable.Empty<TreeNode>()); Children = new List<TreeNode>(children ?? Enumerable.Empty<TreeNode>());
}
/// <summary>
/// Gets the children of this node.
/// </summary>
public List<TreeNode> Children
{
get => _children;
}
/// <summary>
/// Adds a child to the end of the node's list of children.
/// </summary>
/// <param name="child">Child to be added.</param>
public void AddChild(TreeNode child)
{
_children.Add(child);
} }
/// <inheritdoc/> /// <inheritdoc/>