diff --git a/docs/input/assets/images/tree.png b/docs/input/assets/images/tree.png new file mode 100644 index 0000000..fe435fc Binary files /dev/null and b/docs/input/assets/images/tree.png differ diff --git a/docs/input/widgets/barchart.md b/docs/input/widgets/barchart.md index 6e1a993..e76c188 100644 --- a/docs/input/widgets/barchart.md +++ b/docs/input/widgets/barchart.md @@ -1,5 +1,5 @@ Title: Bar Chart -Order: 1 +Order: 20 --- Use `BarChart` to render bar charts to the console. diff --git a/docs/input/widgets/calendar.md b/docs/input/widgets/calendar.md index 3f3ad77..de92f5d 100644 --- a/docs/input/widgets/calendar.md +++ b/docs/input/widgets/calendar.md @@ -1,5 +1,5 @@ Title: Calendar -Order: 3 +Order: 40 RedirectFrom: calendar --- diff --git a/docs/input/widgets/canvas-image.md b/docs/input/widgets/canvas-image.md index 24fd8d4..3e953c4 100644 --- a/docs/input/widgets/canvas-image.md +++ b/docs/input/widgets/canvas-image.md @@ -1,5 +1,5 @@ Title: Canvas Image -Order: 6 +Order: 70 --- To add [ImageSharp](https://github.com/SixLabors/ImageSharp) superpowers to diff --git a/docs/input/widgets/canvas.md b/docs/input/widgets/canvas.md index 847f50e..b362743 100644 --- a/docs/input/widgets/canvas.md +++ b/docs/input/widgets/canvas.md @@ -1,5 +1,5 @@ Title: Canvas -Order: 5 +Order: 60 --- `Canvas` is a widget that allows you to render arbitrary "pixels" diff --git a/docs/input/widgets/figlet.md b/docs/input/widgets/figlet.md index 762ea0d..2518d24 100644 --- a/docs/input/widgets/figlet.md +++ b/docs/input/widgets/figlet.md @@ -1,5 +1,5 @@ Title: Figlet -Order: 4 +Order: 50 RedirectFrom: figlet --- diff --git a/docs/input/widgets/rule.md b/docs/input/widgets/rule.md index ff6ac55..c547f23 100644 --- a/docs/input/widgets/rule.md +++ b/docs/input/widgets/rule.md @@ -1,5 +1,5 @@ Title: Rule -Order: 2 +Order: 30 RedirectFrom: rule --- diff --git a/docs/input/widgets/tree.md b/docs/input/widgets/tree.md new file mode 100644 index 0000000..b2cd873 --- /dev/null +++ b/docs/input/widgets/tree.md @@ -0,0 +1,70 @@ +Title: Tree +Order: 10 +--- + +The `Tree` widget can be used to render hierarchical data. + + + +# Usage + +```csharp +// Create the tree +var tree = new Tree("Root"); + +// Add some nodes +var foo = tree.AddNode("[yellow]Foo[/]"); +var table = foo.AddNode(new Table() + .RoundedBorder() + .AddColumn("First") + .AddColumn("Second") + .AddRow("1", "2") + .AddRow("3", "4") + .AddRow("5", "6")); + +table.AddNode("[blue]Baz[/]"); +foo.AddNode("Qux"); + +var bar = tree.AddNode("[yellow]Bar[/]"); +bar.AddNode(new Calendar(2020, 12) + .AddCalendarEvent(2020, 12, 12) + .HideHeader()); + +// Render the tree +AnsiConsole.Render(root); +``` + +# Collapsing nodes + +```csharp +root.AddNode("Label").Collapsed(); +``` + +# Appearance + +## Style + +```csharp +var root = new Tree("Root") + .Style("white on red"); +``` + +## Guide lines + +```csharp +// ASCII guide lines +var root = new Tree("Root") + .Guide(TreeGuide.Ascii); + +// Default guide lines +var root = new Tree("Root") + .Guide(TreeGuide.Line); + +// Double guide lines +var root = new Tree("Root") + .Guide(TreeGuide.DoubleLine); + +// Bold guide lines +var root = new Tree("Root") + .Guide(TreeGuide.BoldLine); +``` \ No newline at end of file diff --git a/examples/Console/Prompt/Program.cs b/examples/Console/Prompt/Program.cs index edd83de..40874e6 100644 --- a/examples/Console/Prompt/Program.cs +++ b/examples/Console/Prompt/Program.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Spectre.Console; namespace Cursor diff --git a/examples/Console/Trees/Program.cs b/examples/Console/Trees/Program.cs index 5e46818..33747cd 100644 --- a/examples/Console/Trees/Program.cs +++ b/examples/Console/Trees/Program.cs @@ -6,42 +6,40 @@ namespace TableExample { public static void Main() { - var tree = new Tree(); - - tree.AddNode(new FigletText("Dec 2020")); - tree.AddNode("[link]Click to go to summary[/]"); - - // Add the calendar nodes - tree.AddNode("[blue]Calendar[/]", - node => node.AddNode( - new Calendar(2020, 12) - .AddCalendarEvent(2020, 12, 12) - .HideHeader())); - - // Add video games node - tree.AddNode("[red]Played video games[/]", - node => node.AddNode( - new Table() - .RoundedBorder() - .AddColumn("Title") - .AddColumn("Console") - .AddRow("The Witcher 3", "XBox One X") - .AddRow("Cyberpunk 2077", "PC") - .AddRow("Animal Crossing", "Nintendo Switch"))); - - - // Add the fruit nodes - tree.AddNode("[green]Fruits[/]", fruits => - fruits.AddNode("Eaten", - node => node.AddNode( - new BarChart().Width(40) - .AddItem("Apple", 12, Color.Red) - .AddItem("Kiwi", 3, Color.Green) - .AddItem("Banana", 21, Color.Yellow)))); - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine("[yellow]Monthly summary[/]"); + + // Render the tree + var tree = BuildTree(); AnsiConsole.Render(tree); } + + private static Tree BuildTree() + { + // Create the tree + var tree = new Tree("Root") + .Style(Style.Parse("red")) + .Guide(TreeGuide.BoldLine); + + // Add some nodes + var foo = tree.AddNode("[yellow]Foo[/]"); + var table = foo.AddNode(new Table() + .RoundedBorder() + .AddColumn("First") + .AddColumn("Second") + .AddRow("1", "2") + .AddRow("3", "4") + .AddRow("5", "6")); + + table.AddNode("[blue]Baz[/]"); + foo.AddNode("Qux"); + + var bar = tree.AddNode("[yellow]Bar[/]"); + bar.AddNode(new Calendar(2020, 12) + .AddCalendarEvent(2020, 12, 12) + .HideHeader()); + + // Return the tree + return tree; + } } } diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/MultipleRoots.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Tree/MultipleRoots.Output.verified.txt deleted file mode 100644 index a8f06d1..0000000 --- a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/MultipleRoots.Output.verified.txt +++ /dev/null @@ -1,43 +0,0 @@ -├── Root node -│ ├── child1 -│ │ ├── multiple -│ │ │ line 0 -│ │ ├── multiple -│ │ │ line 1 -│ │ ├── multiple -│ │ │ line 2 -│ │ ├── multiple -│ │ │ line 3 -│ │ ├── multiple -│ │ │ line 4 -│ │ ├── multiple -│ │ │ line 5 -│ │ ├── multiple -│ │ │ line 6 -│ │ ├── multiple -│ │ │ line 7 -│ │ ├── multiple -│ │ │ line 8 -│ │ └── multiple -│ │ line 9 -│ ├── child2 -│ │ └── child2Child -│ │ └── Child 2 child -│ │ child -│ └── child3 -│ └── single leaf -│ multiline -│ └── 2020 January -│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ -│ │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ -│ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ -│ │ │ │ │ 1 │ 2 │ 3 │ 4 │ -│ │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ -│ │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ -│ │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ -│ │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │ -│ │ │ │ │ │ │ │ │ -│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ -└── child2Child - └── Child 2 child - child diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render.Output.verified.txt new file mode 100644 index 0000000..1d3ba98 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render.Output.verified.txt @@ -0,0 +1,41 @@ +Root node +╠══ child1 +║ ╠══ multiple +║ ║ line 0 +║ ╠══ multiple +║ ║ line 1 +║ ╠══ multiple +║ ║ line 2 +║ ╠══ multiple +║ ║ line 3 +║ ╠══ multiple +║ ║ line 4 +║ ╠══ multiple +║ ║ line 5 +║ ╠══ multiple +║ ║ line 6 +║ ╠══ multiple +║ ║ line 7 +║ ╠══ multiple +║ ║ line 8 +║ ╚══ multiple +║ line 9 +╠══ child2 +║ ╚══ child2-1 +║ ╚══ Child2-1-1 +║ child +╠══ child3 +║ ╚══ single leaf +║ multiline +║ ╚══ 2021 January +║ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ +║ │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ +║ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ +║ │ │ │ │ │ │ 1 │ 2 │ +║ │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ +║ │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │ +║ │ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ +║ │ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ +║ │ 31 │ │ │ │ │ │ │ +║ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ +╚══ child4 diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/OnlyRoot.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render_NoChildren.Output.verified.txt similarity index 100% rename from src/Spectre.Console.Tests/Expectations/Widgets/Tree/OnlyRoot.Output.verified.txt rename to src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render_NoChildren.Output.verified.txt diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/SingleRoot.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Tree/SingleRoot.Output.verified.txt deleted file mode 100644 index 9d1ef68..0000000 --- a/src/Spectre.Console.Tests/Expectations/Widgets/Tree/SingleRoot.Output.verified.txt +++ /dev/null @@ -1,40 +0,0 @@ -Root node -├── child1 -│ ├── multiple -│ │ line 0 -│ ├── multiple -│ │ line 1 -│ ├── multiple -│ │ line 2 -│ ├── multiple -│ │ line 3 -│ ├── multiple -│ │ line 4 -│ ├── multiple -│ │ line 5 -│ ├── multiple -│ │ line 6 -│ ├── multiple -│ │ line 7 -│ ├── multiple -│ │ line 8 -│ └── multiple -│ line 9 -├── child2 -│ └── child2Child -│ └── Child 2 child -│ child -└── child3 - └── single leaf - multiline - └── 2020 January - ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ - │ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ - ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ - │ │ │ │ 1 │ 2 │ 3 │ 4 │ - │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ - │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ - │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ - │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │ - │ │ │ │ │ │ │ │ - └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj index a687627..34cda0a 100644 --- a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -33,4 +33,8 @@ + + + + diff --git a/src/Spectre.Console.Tests/Unit/TreeTests.cs b/src/Spectre.Console.Tests/Unit/TreeTests.cs index 672583e..eb5001d 100644 --- a/src/Spectre.Console.Tests/Unit/TreeTests.cs +++ b/src/Spectre.Console.Tests/Unit/TreeTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Spectre.Console.Testing; @@ -13,25 +12,28 @@ namespace Spectre.Console.Tests.Unit public class TreeTests { [Fact] - [Expectation("SingleRoot")] - public Task Should_Render_Tree_With_Single_Root_Correctly() + [Expectation("Render")] + 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 tree = new Tree(new Text("Root node")).Guide(TreeGuide.DoubleLine); + + var nestedChildren = Enumerable.Range(0, 10).Select(x => new Text($"multiple\nline {x}")); var child2 = new TreeNode(new Text("child2")); - var child2Child = new TreeNode(new Text("child2Child")); + var child2Child = new TreeNode(new Text("child2-1")); child2.AddNode(child2Child); - child2Child.AddNode(new TreeNode(new Text("Child 2 child\n child"))); + child2Child.AddNode(new TreeNode(new Text("Child2-1-1\nchild"))); var child3 = new TreeNode(new Text("child3")); - var child3Child = new TreeNode(new Text("single leaf\n multiline")); - child3Child.AddNode(new TreeNode(new Calendar(2020, 01))); + var child3Child = new TreeNode(new Text("single leaf\nmultiline")); + child3Child.AddNode(new TreeNode(new Calendar(2021, 01))); child3.AddNode(child3Child); - var children = new List { new(new Text("child1"), nestedChildren), child2, child3 }; - var root = new TreeNode(new Text("Root node"), children); - var tree = new Tree().AddNode(root); + + tree.AddNode("child1").AddNodes(nestedChildren); + tree.AddNode(child2); + tree.AddNode(child3); + tree.AddNode("child4"); // When console.Render(tree); @@ -41,41 +43,12 @@ namespace Spectre.Console.Tests.Unit } [Fact] - [Expectation("MultipleRoots")] - public Task Should_Render_Tree_With_Multiple_Roots_Correctly() + [Expectation("Render_NoChildren")] + public Task Should_Render_Tree_With_No_Child_Nodes_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.AddNode(child2Child); - child2Child.AddNode(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.AddNode(new TreeNode(new Calendar(2020, 01))); - child3.AddNode(child3Child); - var children = new List { new(new Text("child1"), nestedChildren), child2, child3 }; - var root = new TreeNode(new Text("Root node"), children); - var tree = new Tree().AddNode(root).AddNode(child2Child); - - // When - console.Render(tree); - - // Then - return Verifier.Verify(console.Output); - } - - [Fact] - [Expectation("OnlyRoot")] - 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()); - var tree = new Tree().AddNode(root); + var tree = new Tree(new Text("Root node")); // When console.Render(tree); diff --git a/src/Spectre.Console/Extensions/HasTreeNodeExtensions.cs b/src/Spectre.Console/Extensions/HasTreeNodeExtensions.cs index a45d949..0417c2f 100644 --- a/src/Spectre.Console/Extensions/HasTreeNodeExtensions.cs +++ b/src/Spectre.Console/Extensions/HasTreeNodeExtensions.cs @@ -1,23 +1,24 @@ using System; +using System.Collections.Generic; using System.Linq; using Spectre.Console.Rendering; namespace Spectre.Console { /// - /// Contains extension methods for . + /// Contains extension methods for . /// public static class HasTreeNodeExtensions { /// /// Adds a tree node. /// - /// An object type with tree nodes. - /// The object that has tree nodes. + /// An object with tree nodes. + /// The object to add the tree node to. /// The node's markup text. - /// The same instance so that multiple calls can be chained. - public static T AddNode(this T obj, string markup) - where T : class, IHasTreeNodes + /// The added tree node. + public static TreeNode AddNode(this T obj, string markup) + where T : IHasTreeNodes { if (obj is null) { @@ -35,31 +36,12 @@ namespace Spectre.Console /// /// Adds a tree node. /// - /// An object type with tree nodes. - /// The object that has tree nodes. - /// The node's markup text. - /// An action that can be used to configure the created node further. - /// The same instance so that multiple calls can be chained. - public static T AddNode(this T obj, string markup, Action action) - where T : class, IHasTreeNodes - { - if (markup is null) - { - throw new ArgumentNullException(nameof(markup)); - } - - return AddNode(obj, new Markup(markup), action); - } - - /// - /// Adds a tree node. - /// - /// An object type with tree nodes. - /// The object that has tree nodes. + /// An object with tree nodes. + /// The object to add the tree node to. /// The renderable to add. - /// The same instance so that multiple calls can be chained. - public static T AddNode(this T obj, IRenderable renderable) - where T : class, IHasTreeNodes + /// The added tree node. + public static TreeNode AddNode(this T obj, IRenderable renderable) + where T : IHasTreeNodes { if (obj is null) { @@ -71,52 +53,20 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(renderable)); } - obj.Children.Add(new TreeNode(renderable)); - return obj; - } - - /// - /// Adds a tree node. - /// - /// An object type with tree nodes. - /// The object that has tree nodes. - /// The renderable to add. - /// An action that can be used to configure the created node further. - /// The same instance so that multiple calls can be chained. - public static T AddNode(this T obj, IRenderable renderable, Action action) - where T : class, IHasTreeNodes - { - if (obj is null) - { - throw new ArgumentNullException(nameof(obj)); - } - - if (renderable is null) - { - throw new ArgumentNullException(nameof(renderable)); - } - - if (action is null) - { - throw new ArgumentNullException(nameof(action)); - } - var node = new TreeNode(renderable); - action(node); - - obj.Children.Add(node); - return obj; + obj.Nodes.Add(node); + return node; } /// /// Adds a tree node. /// - /// An object type with tree nodes. - /// The object that has tree nodes. + /// An object with tree nodes. + /// The object to add the tree node to. /// The tree node to add. - /// The same instance so that multiple calls can be chained. - public static T AddNode(this T obj, TreeNode node) - where T : class, IHasTreeNodes + /// The added tree node. + public static TreeNode AddNode(this T obj, TreeNode node) + where T : IHasTreeNodes { if (obj is null) { @@ -128,19 +78,18 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(node)); } - obj.Children.Add(node); - return obj; + obj.Nodes.Add(node); + return node; } /// /// Add multiple tree nodes. /// - /// An object type with tree nodes. - /// The object that has tree nodes. + /// An object with tree nodes. + /// The object to add the tree nodes to. /// The tree nodes to add. - /// The same instance so that multiple calls can be chained. - public static T AddNodes(this T obj, params string[] nodes) - where T : class, IHasTreeNodes + public static void AddNodes(this T obj, params string[] nodes) + where T : IHasTreeNodes { if (obj is null) { @@ -152,19 +101,17 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(nodes)); } - obj.Children.AddRange(nodes.Select(node => new TreeNode(new Markup(node)))); - return obj; + obj.Nodes.AddRange(nodes.Select(node => new TreeNode(new Markup(node)))); } /// /// Add multiple tree nodes. /// - /// An object type with tree nodes. - /// The object that has tree nodes. + /// An object with tree nodes. + /// The object to add the tree nodes to. /// The tree nodes to add. - /// The same instance so that multiple calls can be chained. - public static T AddNodes(this T obj, params TreeNode[] nodes) - where T : class, IHasTreeNodes + public static void AddNodes(this T obj, IEnumerable nodes) + where T : IHasTreeNodes { if (obj is null) { @@ -176,8 +123,95 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(nodes)); } - obj.Children.AddRange(nodes); - return obj; + obj.Nodes.AddRange(nodes.Select(node => new TreeNode(new Markup(node)))); + } + + /// + /// Add multiple tree nodes. + /// + /// An object with tree nodes. + /// The object to add the tree nodes to. + /// The tree nodes to add. + public static void AddNodes(this T obj, params IRenderable[] nodes) + where T : IHasTreeNodes + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (nodes is null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + obj.Nodes.AddRange(nodes.Select(node => new TreeNode(node))); + } + + /// + /// Add multiple tree nodes. + /// + /// An object with tree nodes. + /// The object to add the tree nodes to. + /// The tree nodes to add. + public static void AddNodes(this T obj, IEnumerable nodes) + where T : IHasTreeNodes + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (nodes is null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + obj.Nodes.AddRange(nodes.Select(node => new TreeNode(node))); + } + + /// + /// Add multiple tree nodes. + /// + /// An object with tree nodes. + /// The object to add the tree nodes to. + /// The tree nodes to add. + public static void AddNodes(this T obj, params TreeNode[] nodes) + where T : IHasTreeNodes + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (nodes is null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + obj.Nodes.AddRange(nodes); + } + + /// + /// Add multiple tree nodes. + /// + /// An object with tree nodes. + /// The object to add the tree nodes to. + /// The tree nodes to add. + public static void AddNodes(this T obj, IEnumerable nodes) + where T : IHasTreeNodes + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (nodes is null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + obj.Nodes.AddRange(nodes); } } } diff --git a/src/Spectre.Console/Extensions/ListExtensions.cs b/src/Spectre.Console/Extensions/ListExtensions.cs new file mode 100644 index 0000000..ecc66d2 --- /dev/null +++ b/src/Spectre.Console/Extensions/ListExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console +{ + internal static class ListExtensions + { + public static void RemoveLast(this List list) + { + if (list is null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (list.Count > 0) + { + list.RemoveAt(list.Count - 1); + } + } + + public static void AddOrReplaceLast(this List list, T item) + { + if (list is null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (list.Count == 0) + { + list.Add(item); + } + else + { + list[list.Count - 1] = item; + } + } + } +} diff --git a/src/Spectre.Console/Extensions/TreeExtensions.cs b/src/Spectre.Console/Extensions/TreeExtensions.cs new file mode 100644 index 0000000..959a640 --- /dev/null +++ b/src/Spectre.Console/Extensions/TreeExtensions.cs @@ -0,0 +1,44 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class TreeExtensions + { + /// + /// Sets the tree style. + /// + /// The tree. + /// The tree style. + /// The same instance so that multiple calls can be chained. + public static Tree Style(this Tree tree, Style? style) + { + if (tree is null) + { + throw new ArgumentNullException(nameof(tree)); + } + + tree.Style = style; + return tree; + } + + /// + /// Sets the tree guide line appearance. + /// + /// The tree. + /// The tree guide lines to use. + /// The same instance so that multiple calls can be chained. + public static Tree Guide(this Tree tree, TreeGuide guide) + { + if (tree is null) + { + throw new ArgumentNullException(nameof(tree)); + } + + tree.Guide = guide; + return tree; + } + } +} diff --git a/src/Spectre.Console/Extensions/TreeGuideExtensions.cs b/src/Spectre.Console/Extensions/TreeGuideExtensions.cs new file mode 100644 index 0000000..2a7ef4e --- /dev/null +++ b/src/Spectre.Console/Extensions/TreeGuideExtensions.cs @@ -0,0 +1,31 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// Contains extension methods for . + /// + public static class TreeGuideExtensions + { + /// + /// Gets the safe border for a border. + /// + /// The tree guide to get the safe version for. + /// Whether or not to return the safe border. + /// The safe border if one exist, otherwise the original border. + public static TreeGuide GetSafeTreeGuide(this TreeGuide guide, bool safe) + { + if (guide is null) + { + throw new ArgumentNullException(nameof(guide)); + } + + if (safe && guide.SafeTreeGuide != null) + { + return guide.SafeTreeGuide; + } + + return guide; + } + } +} diff --git a/src/Spectre.Console/Extensions/TreeNodeExtensions.cs b/src/Spectre.Console/Extensions/TreeNodeExtensions.cs new file mode 100644 index 0000000..a58a8cd --- /dev/null +++ b/src/Spectre.Console/Extensions/TreeNodeExtensions.cs @@ -0,0 +1,47 @@ +using System; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class TreeNodeExtensions + { + /// + /// Expands the tree. + /// + /// The tree node. + /// The same instance so that multiple calls can be chained. + public static TreeNode Expand(this TreeNode node) + { + return Expand(node, true); + } + + /// + /// Collapses the tree. + /// + /// The tree node. + /// The same instance so that multiple calls can be chained. + public static TreeNode Collapse(this TreeNode node) + { + return Expand(node, false); + } + + /// + /// Sets whether or not the tree node should be expanded. + /// + /// The tree node. + /// Whether or not the tree node should be expanded. + /// The same instance so that multiple calls can be chained. + public static TreeNode Expand(this TreeNode node, bool expand) + { + if (node is null) + { + throw new ArgumentNullException(nameof(node)); + } + + node.Expanded = expand; + return node; + } + } +} diff --git a/src/Spectre.Console/IHasTreeNodes.cs b/src/Spectre.Console/IHasTreeNodes.cs index bbed370..5b16994 100644 --- a/src/Spectre.Console/IHasTreeNodes.cs +++ b/src/Spectre.Console/IHasTreeNodes.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Spectre.Console { @@ -8,8 +8,8 @@ namespace Spectre.Console public interface IHasTreeNodes { /// - /// Gets the children of this node. + /// Gets the tree's child nodes. /// - public List Children { get; } + List Nodes { get; } } -} +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Tree/AsciiTreeAppearance.cs b/src/Spectre.Console/Rendering/Tree/AsciiTreeAppearance.cs deleted file mode 100644 index 2e4d236..0000000 --- a/src/Spectre.Console/Rendering/Tree/AsciiTreeAppearance.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Spectre.Console.Rendering -{ - /// - /// An ASCII rendering of a tree. - /// - public sealed class AsciiTreeAppearance : TreeAppearance - { - /// - public override int PartSize => 4; - - /// - public override string GetPart(TreePart part) - { - return part switch - { - TreePart.SiblingConnector => "│ ", - TreePart.ChildBranch => "├── ", - TreePart.BottomChildBranch => "└── ", - _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), - }; - } - } -} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Tree/AsciiTreeGuide.cs b/src/Spectre.Console/Rendering/Tree/AsciiTreeGuide.cs new file mode 100644 index 0000000..50dc083 --- /dev/null +++ b/src/Spectre.Console/Rendering/Tree/AsciiTreeGuide.cs @@ -0,0 +1,23 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// An ASCII tree guide. + /// + public sealed class AsciiTreeGuide : TreeGuide + { + /// + public override string GetPart(TreeGuidePart part) + { + return part switch + { + TreeGuidePart.Space => " ", + TreeGuidePart.Continue => "| ", + TreeGuidePart.Fork => "|-- ", + TreeGuidePart.End => "`-- ", + _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), + }; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Tree/BoldLineTreeGuide.cs b/src/Spectre.Console/Rendering/Tree/BoldLineTreeGuide.cs new file mode 100644 index 0000000..164f0b7 --- /dev/null +++ b/src/Spectre.Console/Rendering/Tree/BoldLineTreeGuide.cs @@ -0,0 +1,26 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// A tree guide made up of bold lines. + /// + public sealed class BoldLineTreeGuide : TreeGuide + { + /// + public override TreeGuide? SafeTreeGuide => Ascii; + + /// + public override string GetPart(TreeGuidePart part) + { + return part switch + { + TreeGuidePart.Space => " ", + TreeGuidePart.Continue => "┃ ", + TreeGuidePart.Fork => "┣━━ ", + TreeGuidePart.End => "┗━━ ", + _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), + }; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Tree/DoubleLineTreeGuide.cs b/src/Spectre.Console/Rendering/Tree/DoubleLineTreeGuide.cs new file mode 100644 index 0000000..73fb164 --- /dev/null +++ b/src/Spectre.Console/Rendering/Tree/DoubleLineTreeGuide.cs @@ -0,0 +1,26 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// A tree guide made up of double lines. + /// + public sealed class DoubleLineTreeGuide : TreeGuide + { + /// + public override TreeGuide? SafeTreeGuide => Ascii; + + /// + public override string GetPart(TreeGuidePart part) + { + return part switch + { + TreeGuidePart.Space => " ", + TreeGuidePart.Continue => "║ ", + TreeGuidePart.Fork => "╠══ ", + TreeGuidePart.End => "╚══ ", + _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), + }; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Tree/LineTreeGuide.cs b/src/Spectre.Console/Rendering/Tree/LineTreeGuide.cs new file mode 100644 index 0000000..f3e73e0 --- /dev/null +++ b/src/Spectre.Console/Rendering/Tree/LineTreeGuide.cs @@ -0,0 +1,26 @@ +using System; + +namespace Spectre.Console.Rendering +{ + /// + /// A tree guide made up of lines. + /// + public sealed class LineTreeGuide : TreeGuide + { + /// + public override TreeGuide? SafeTreeGuide => Ascii; + + /// + public override string GetPart(TreeGuidePart part) + { + return part switch + { + TreeGuidePart.Space => " ", + TreeGuidePart.Continue => "│ ", + TreeGuidePart.Fork => "├── ", + TreeGuidePart.End => "└── ", + _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), + }; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/TreePart.cs b/src/Spectre.Console/Rendering/TreeGuidePart.cs similarity index 72% rename from src/Spectre.Console/Rendering/TreePart.cs rename to src/Spectre.Console/Rendering/TreeGuidePart.cs index ea74582..282a8bc 100644 --- a/src/Spectre.Console/Rendering/TreePart.cs +++ b/src/Spectre.Console/Rendering/TreeGuidePart.cs @@ -3,21 +3,26 @@ namespace Spectre.Console.Rendering /// /// Defines the different rendering parts of a . /// - public enum TreePart + public enum TreeGuidePart { + /// + /// Represents a space. + /// + Space, + /// /// Connection between siblings. /// - SiblingConnector, + Continue, /// /// Branch from parent to child. /// - ChildBranch, + Fork, /// /// Branch from parent to child for the last child in a set. /// - BottomChildBranch, + End, } } \ No newline at end of file diff --git a/src/Spectre.Console/TreeAppearance.Known.cs b/src/Spectre.Console/TreeAppearance.Known.cs deleted file mode 100644 index 4cbb85c..0000000 --- a/src/Spectre.Console/TreeAppearance.Known.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Spectre.Console.Rendering; - -namespace Spectre.Console -{ - /// - /// Represents a tree appearance. - /// - public abstract partial class TreeAppearance - { - /// - /// Gets ASCII rendering of a tree. - /// - public static TreeAppearance Ascii { get; } = new AsciiTreeAppearance(); - } -} diff --git a/src/Spectre.Console/TreeGuide.Known.cs b/src/Spectre.Console/TreeGuide.Known.cs new file mode 100644 index 0000000..f25387b --- /dev/null +++ b/src/Spectre.Console/TreeGuide.Known.cs @@ -0,0 +1,30 @@ +using Spectre.Console.Rendering; + +namespace Spectre.Console +{ + /// + /// Represents tree guide lines. + /// + public abstract partial class TreeGuide + { + /// + /// Gets an instance. + /// + public static TreeGuide Ascii { get; } = new AsciiTreeGuide(); + + /// + /// Gets a instance. + /// + public static TreeGuide Line { get; } = new LineTreeGuide(); + + /// + /// Gets a instance. + /// + public static TreeGuide DoubleLine { get; } = new DoubleLineTreeGuide(); + + /// + /// Gets a instance. + /// + public static TreeGuide BoldLine { get; } = new BoldLineTreeGuide(); + } +} diff --git a/src/Spectre.Console/TreeAppearance.cs b/src/Spectre.Console/TreeGuide.cs similarity index 58% rename from src/Spectre.Console/TreeAppearance.cs rename to src/Spectre.Console/TreeGuide.cs index 077c745..1a25522 100644 --- a/src/Spectre.Console/TreeAppearance.cs +++ b/src/Spectre.Console/TreeGuide.cs @@ -3,20 +3,20 @@ using Spectre.Console.Rendering; namespace Spectre.Console { /// - /// Represents a tree appearance. + /// Represents tree guide lines. /// - public abstract partial class TreeAppearance + public abstract partial class TreeGuide { /// - /// Gets the length of all tree part strings. + /// Gets the safe guide lines or null if none exist. /// - public abstract int PartSize { get; } + public virtual TreeGuide? SafeTreeGuide { get; } /// - /// Get the set of characters used to render the corresponding . + /// Get the set of characters used to render the corresponding . /// /// The part of the tree to get rendering string for. /// Rendering string for the tree part. - public abstract string GetPart(TreePart part); + public abstract string GetPart(TreeGuidePart part); } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/BarChart.cs b/src/Spectre.Console/Widgets/BarChart.cs index 3541311..098f863 100644 --- a/src/Spectre.Console/Widgets/BarChart.cs +++ b/src/Spectre.Console/Widgets/BarChart.cs @@ -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); } } } diff --git a/src/Spectre.Console/Widgets/Tree.cs b/src/Spectre.Console/Widgets/Tree.cs index dd9a5c5..b613831 100644 --- a/src/Spectre.Console/Widgets/Tree.cs +++ b/src/Spectre.Console/Widgets/Tree.cs @@ -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 { /// - /// Representation of tree data. + /// A renderable tree. /// public sealed class Tree : Renderable, IHasTreeNodes { + private readonly TreeNode _root; + /// /// Gets or sets the tree style. /// - public Style Style { get; set; } = Style.Plain; + public Style? Style { get; set; } /// - /// Gets or sets the appearance of the tree. + /// Gets or sets the tree guide lines. /// - public TreeAppearance Appearance { get; set; } = TreeAppearance.Ascii; + public TreeGuide Guide { get; set; } = TreeGuide.Line; /// - /// Gets the tree nodes. + /// Gets the tree's child nodes. /// - public List Nodes { get; } + public List Nodes { get; } = new List(); + + /// + /// Gets or sets a value indicating whether or not the tree is expanded or not. + /// + public bool Expanded { get; set; } = true; /// - List IHasTreeNodes.Children => Nodes; + List IHasTreeNodes.Nodes => _root.Nodes; /// /// Initializes a new instance of the class. /// - public Tree() + /// The tree label. + public Tree(IRenderable renderable) { - Nodes = new List(); + _root = new TreeNode(renderable); } - /// - protected override Measurement Measure(RenderContext context, int maxWidth) + /// + /// Initializes a new instance of the class. + /// + /// The tree label. + 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)); } /// protected override IEnumerable Render(RenderContext context, int maxWidth) { - if (Nodes.Count == 1) + var result = new List(); + + var stack = new Stack>(); + stack.Push(new Queue(new[] { _root })); + + var levels = new List(); + levels.Add(GetGuide(context, TreeGuidePart.Continue)); + + while (stack.Count > 0) { - // Single root - return Nodes[0] - .Render(context, maxWidth) - .Concat(new List { 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() - .Concat(RenderChildren( - context, maxWidth - Appearance.PartSize, root, - depth: 0)); - } - } + var isLastChild = stackNode.Count == 1; + var current = stackNode.Dequeue(); - private IEnumerable RenderChildren( - RenderContext context, int maxWidth, TreeNode node, - int depth, int? trailingStarted = null) - { - var result = new List(); - 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(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); + } } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/TreeNode.cs b/src/Spectre.Console/Widgets/TreeNode.cs index d7c19d2..188504f 100644 --- a/src/Spectre.Console/Widgets/TreeNode.cs +++ b/src/Spectre.Console/Widgets/TreeNode.cs @@ -1,41 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using Spectre.Console.Rendering; namespace Spectre.Console { /// - /// Node of a tree. + /// Represents a tree node. /// - public sealed class TreeNode : IHasTreeNodes, IRenderable + public sealed class TreeNode : IHasTreeNodes { - private readonly IRenderable _renderable; + internal IRenderable Renderable { get; } - /// - public List Children { get; } + /// + /// Gets the tree node's child nodes. + /// + public List Nodes { get; } = new List(); + + /// + /// Gets or sets a value indicating whether or not the tree node is expanded or not. + /// + public bool Expanded { get; set; } = true; /// /// Initializes a new instance of the class. /// - /// The which this node wraps. - /// Any children that the node is declared with. - public TreeNode(IRenderable renderable, IEnumerable? children = null) + /// The tree node label. + public TreeNode(IRenderable renderable) { - _renderable = renderable ?? throw new ArgumentNullException(nameof(renderable)); - Children = new List(children ?? Enumerable.Empty()); - } - - /// - public Measurement Measure(RenderContext context, int maxWidth) - { - return _renderable.Measure(context, maxWidth); - } - - /// - public IEnumerable Render(RenderContext context, int maxWidth) - { - return _renderable.Render(context, maxWidth); + Renderable = renderable; } } } \ No newline at end of file