mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-01 01:25:27 +08:00 
			
		
		
		
	Add tree widget
This commit is contained in:
		| @@ -0,0 +1,40 @@ | |||||||
|  | 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  │     │ | ||||||
|  |             │     │     │     │     │     │     │     │ | ||||||
|  |             └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | Root node | ||||||
							
								
								
									
										90
									
								
								src/Spectre.Console.Tests/Unit/TreeMeasureTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/Spectre.Console.Tests/Unit/TreeMeasureTests.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								src/Spectre.Console.Tests/Unit/TreeRenderingTests.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/Spectre.Console.Tests/Unit/TreeRenderingTests.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -68,7 +68,7 @@ namespace Spectre.Console.Internal | |||||||
|                 case Justify.Right: |                 case Justify.Right: | ||||||
|                     { |                     { | ||||||
|                         var diff = maxWidth - width; |                         var diff = maxWidth - width; | ||||||
|                         segments.Insert(0, new Segment(new string(' ', diff))); |                         segments.Insert(0, Segment.Padding(diff)); | ||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
| @@ -76,14 +76,14 @@ namespace Spectre.Console.Internal | |||||||
|                     { |                     { | ||||||
|                         // Left side. |                         // Left side. | ||||||
|                         var diff = (maxWidth - width) / 2; |                         var diff = (maxWidth - width) / 2; | ||||||
|                         segments.Insert(0, new Segment(new string(' ', diff))); |                         segments.Insert(0, Segment.Padding(diff)); | ||||||
|  |  | ||||||
|                         // Right side |                         // Right side | ||||||
|                         segments.Add(new Segment(new string(' ', diff))); |                         segments.Add(Segment.Padding(diff)); | ||||||
|                         var remainder = (maxWidth - width) % 2; |                         var remainder = (maxWidth - width) % 2; | ||||||
|                         if (remainder != 0) |                         if (remainder != 0) | ||||||
|                         { |                         { | ||||||
|                             segments.Add(new Segment(new string(' ', remainder))); |                             segments.Add(Segment.Padding(remainder)); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         break; |                         break; | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								src/Spectre.Console/Rendering/AsciiTreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/Spectre.Console/Rendering/AsciiTreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | using System; | ||||||
|  |  | ||||||
|  | namespace Spectre.Console.Rendering | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// An ASCII rendering of a tree. | ||||||
|  |     /// </summary> | ||||||
|  |     public sealed class AsciiTreeRendering : ITreeRendering | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc/> | ||||||
|  |         public string GetPart(TreePart part) | ||||||
|  |         { | ||||||
|  |             return part switch | ||||||
|  |             { | ||||||
|  |                 TreePart.SiblingConnector => "│   ", | ||||||
|  |                 TreePart.ChildBranch => "├── ", | ||||||
|  |                 TreePart.BottomChildBranch => "└── ", | ||||||
|  |                 _ => throw new ArgumentOutOfRangeException(nameof(part), part, "Unknown tree part."), | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc/> | ||||||
|  |         public int PartSize => 4; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								src/Spectre.Console/Rendering/ITreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Spectre.Console/Rendering/ITreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | namespace Spectre.Console.Rendering | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Represents the characters used to render a tree. | ||||||
|  |     /// </summary> | ||||||
|  |     public interface ITreeRendering | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Get the set of characters used to render the corresponding <see cref="TreePart"/>. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="part">The part of the tree to get rendering string for.</param> | ||||||
|  |         /// <returns>Rendering string for the tree part.</returns> | ||||||
|  |         string GetPart(TreePart part); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Gets the length of all tree part strings. | ||||||
|  |         /// </summary> | ||||||
|  |         int PartSize { get; } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -52,6 +52,13 @@ namespace Spectre.Console.Rendering | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false, false); |         public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false, false); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Creates padding segment. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="size">Number of whitespace characters.</param> | ||||||
|  |         /// <returns>Segment for specified padding size.</returns> | ||||||
|  |         public static Segment Padding(int size) => new(new string(' ', size)); | ||||||
|  |  | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Initializes a new instance of the <see cref="Segment"/> class. |         /// Initializes a new instance of the <see cref="Segment"/> class. | ||||||
|         /// </summary> |         /// </summary> | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ namespace Spectre.Console.Rendering | |||||||
|                 var missing = Width - length; |                 var missing = Width - length; | ||||||
|                 if (missing > 0) |                 if (missing > 0) | ||||||
|                 { |                 { | ||||||
|                     line.Add(new Segment(new string(' ', missing))); |                     line.Add(Segment.Padding(missing)); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -59,7 +59,7 @@ namespace Spectre.Console.Rendering | |||||||
|                 { |                 { | ||||||
|                     lines.Add(new SegmentLine |                     lines.Add(new SegmentLine | ||||||
|                     { |                     { | ||||||
|                         new Segment(new string(' ', Width)), |                         Segment.Padding(Width), | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src/Spectre.Console/Rendering/TreePart.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/Spectre.Console/Rendering/TreePart.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | namespace Spectre.Console.Rendering | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Defines the different rendering parts of a <see cref="Tree"/>. | ||||||
|  |     /// </summary> | ||||||
|  |     public enum TreePart | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Connection between siblings. | ||||||
|  |         /// </summary> | ||||||
|  |         SiblingConnector, | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Branch from parent to child. | ||||||
|  |         /// </summary> | ||||||
|  |         ChildBranch, | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Branch from parent to child for the last child in a set. | ||||||
|  |         /// </summary> | ||||||
|  |         BottomChildBranch, | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/Spectre.Console/Rendering/TreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Spectre.Console/Rendering/TreeRendering.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | 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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -60,7 +60,7 @@ namespace Spectre.Console | |||||||
|  |  | ||||||
|                         if (lineWidth < maxWidth) |                         if (lineWidth < maxWidth) | ||||||
|                         { |                         { | ||||||
|                             yield return new Segment(new string(' ', maxWidth - lineWidth)); |                             yield return Segment.Padding(maxWidth - lineWidth); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     else if (alignment == Justify.Center) |                     else if (alignment == Justify.Center) | ||||||
| @@ -68,15 +68,15 @@ namespace Spectre.Console | |||||||
|                         var left = (maxWidth - lineWidth) / 2; |                         var left = (maxWidth - lineWidth) / 2; | ||||||
|                         var right = left + ((maxWidth - lineWidth) % 2); |                         var right = left + ((maxWidth - lineWidth) % 2); | ||||||
|  |  | ||||||
|                         yield return new Segment(new string(' ', left)); |                         yield return Segment.Padding(left); | ||||||
|                         yield return line; |                         yield return line; | ||||||
|                         yield return new Segment(new string(' ', right)); |                         yield return Segment.Padding(right); | ||||||
|                     } |                     } | ||||||
|                     else if (alignment == Justify.Right) |                     else if (alignment == Justify.Right) | ||||||
|                     { |                     { | ||||||
|                         if (lineWidth < maxWidth) |                         if (lineWidth < maxWidth) | ||||||
|                         { |                         { | ||||||
|                             yield return new Segment(new string(' ', maxWidth - lineWidth)); |                             yield return Segment.Padding(maxWidth - lineWidth); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         yield return line; |                         yield return line; | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ namespace Spectre.Console | |||||||
|             // Top padding |             // Top padding | ||||||
|             for (var i = 0; i < Padding.GetTopSafe(); i++) |             for (var i = 0; i < Padding.GetTopSafe(); i++) | ||||||
|             { |             { | ||||||
|                 result.Add(new Segment(new string(' ', width))); |                 result.Add(Segment.Padding(width)); | ||||||
|                 result.Add(Segment.LineBreak); |                 result.Add(Segment.LineBreak); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -76,7 +76,7 @@ namespace Spectre.Console | |||||||
|                 // Left padding |                 // Left padding | ||||||
|                 if (Padding.GetLeftSafe() != 0) |                 if (Padding.GetLeftSafe() != 0) | ||||||
|                 { |                 { | ||||||
|                     result.Add(new Segment(new string(' ', Padding.GetLeftSafe()))); |                     result.Add(Segment.Padding(Padding.GetLeftSafe())); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 result.AddRange(line); |                 result.AddRange(line); | ||||||
| @@ -84,7 +84,7 @@ namespace Spectre.Console | |||||||
|                 // Right padding |                 // Right padding | ||||||
|                 if (Padding.GetRightSafe() != 0) |                 if (Padding.GetRightSafe() != 0) | ||||||
|                 { |                 { | ||||||
|                     result.Add(new Segment(new string(' ', Padding.GetRightSafe()))); |                     result.Add(Segment.Padding(Padding.GetRightSafe())); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 // Missing space on right side? |                 // Missing space on right side? | ||||||
| @@ -92,7 +92,7 @@ namespace Spectre.Console | |||||||
|                 var diff = width - lineWidth - Padding.GetLeftSafe() - Padding.GetRightSafe(); |                 var diff = width - lineWidth - Padding.GetLeftSafe() - Padding.GetRightSafe(); | ||||||
|                 if (diff > 0) |                 if (diff > 0) | ||||||
|                 { |                 { | ||||||
|                     result.Add(new Segment(new string(' ', diff))); |                     result.Add(Segment.Padding(diff)); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 result.Add(Segment.LineBreak); |                 result.Add(Segment.LineBreak); | ||||||
| @@ -101,7 +101,7 @@ namespace Spectre.Console | |||||||
|             // Bottom padding |             // Bottom padding | ||||||
|             for (var i = 0; i < Padding.GetBottomSafe(); i++) |             for (var i = 0; i < Padding.GetBottomSafe(); i++) | ||||||
|             { |             { | ||||||
|                 result.Add(new Segment(new string(' ', width))); |                 result.Add(Segment.Padding(width)); | ||||||
|                 result.Add(Segment.LineBreak); |                 result.Add(Segment.LineBreak); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ namespace Spectre.Console | |||||||
|                 if (length < childWidth) |                 if (length < childWidth) | ||||||
|                 { |                 { | ||||||
|                     var diff = childWidth - length; |                     var diff = childWidth - length; | ||||||
|                     content.Add(new Segment(new string(' ', diff))); |                     content.Add(Segment.Padding(diff)); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 result.AddRange(content); |                 result.AddRange(content); | ||||||
|   | |||||||
							
								
								
									
										122
									
								
								src/Spectre.Console/Widgets/Tree.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/Spectre.Console/Widgets/Tree.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using Spectre.Console.Internal; | ||||||
|  | using Spectre.Console.Rendering; | ||||||
|  |  | ||||||
|  | namespace Spectre.Console | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     ///     Representation of tree data. | ||||||
|  |     /// </summary> | ||||||
|  |     public sealed class Tree : Renderable | ||||||
|  |     { | ||||||
|  |         private readonly TreeNode _rootNode; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Gets or sets the tree style. | ||||||
|  |         /// </summary> | ||||||
|  |         public Style Style { get; set; } = Style.Plain; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         ///  Gets or sets the rendering type used for the tree. | ||||||
|  |         /// </summary> | ||||||
|  |         public ITreeRendering Rendering { get; set; } = TreeRendering.Ascii; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Initializes a new instance of the <see cref="Tree"/> class. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="rootNode">Root node of the tree to be rendered.</param> | ||||||
|  |         public Tree(TreeNode rootNode) | ||||||
|  |         { | ||||||
|  |             _rootNode = rootNode; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override Measurement Measure(RenderContext context, int maxWidth) | ||||||
|  |         { | ||||||
|  |             return MeasureAtDepth(context, maxWidth, _rootNode, depth: 0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private Measurement MeasureAtDepth(RenderContext context, int maxWidth, TreeNode node, int depth) | ||||||
|  |         { | ||||||
|  |             var rootMeasurement = node.Measure(context, maxWidth); | ||||||
|  |             var treeIndentation = depth * Rendering.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)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth) | ||||||
|  |         { | ||||||
|  |             return _rootNode | ||||||
|  |                 .Render(context, maxWidth) | ||||||
|  |                 .Concat(new List<Segment> { Segment.LineBreak }) | ||||||
|  |                 .Concat(RenderChildren(context, maxWidth - Rendering.PartSize, _rootNode, depth: 0)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private IEnumerable<Segment> RenderChildren(RenderContext context, int maxWidth, TreeNode node, int depth, | ||||||
|  |             int? trailingStarted = null) | ||||||
|  |         { | ||||||
|  |             var result = new List<Segment>(); | ||||||
|  |  | ||||||
|  |             foreach (var (index, firstChild, lastChild, childNode) in node.Children.Enumerate()) | ||||||
|  |             { | ||||||
|  |                 var lines = Segment.SplitLines(context, childNode.Render(context, maxWidth)); | ||||||
|  |  | ||||||
|  |                 foreach (var (lineIndex, firstLine, lastLine, line) in lines.Enumerate()) | ||||||
|  |                 { | ||||||
|  |                     var siblingConnectorSegment = | ||||||
|  |                         new Segment(Rendering.GetPart(TreePart.SiblingConnector), Style); | ||||||
|  |                     if (trailingStarted != null) | ||||||
|  |                     { | ||||||
|  |                         result.AddRange(Enumerable.Repeat(siblingConnectorSegment, trailingStarted.Value)); | ||||||
|  |                         result.AddRange(Enumerable.Repeat( | ||||||
|  |                             Segment.Padding(Rendering.PartSize), | ||||||
|  |                             depth - trailingStarted.Value)); | ||||||
|  |                     } | ||||||
|  |                     else | ||||||
|  |                     { | ||||||
|  |                         result.AddRange(Enumerable.Repeat(siblingConnectorSegment, depth)); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (firstLine) | ||||||
|  |                     { | ||||||
|  |                         result.Add(lastChild | ||||||
|  |                             ? new Segment(Rendering.GetPart(TreePart.BottomChildBranch), Style) | ||||||
|  |                             : new Segment(Rendering.GetPart(TreePart.ChildBranch), Style)); | ||||||
|  |                     } | ||||||
|  |                     else | ||||||
|  |                     { | ||||||
|  |                         result.Add(lastChild ? Segment.Padding(Rendering.PartSize) : siblingConnectorSegment); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     result.AddRange(line); | ||||||
|  |                     result.Add(Segment.LineBreak); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 var childTrailingStarted = trailingStarted ?? (lastChild ? depth : null); | ||||||
|  |                 result.AddRange(RenderChildren(context, maxWidth - Rendering.PartSize, childNode, depth + 1, | ||||||
|  |                     childTrailingStarted)); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								src/Spectre.Console/Widgets/TreeNode.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/Spectre.Console/Widgets/TreeNode.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using Spectre.Console.Rendering; | ||||||
|  |  | ||||||
|  | namespace Spectre.Console | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Node of a tree. | ||||||
|  |     /// </summary> | ||||||
|  |     public sealed class TreeNode : IRenderable | ||||||
|  |     { | ||||||
|  |         private readonly IRenderable _renderable; | ||||||
|  |         private List<TreeNode> _children; | ||||||
|  |  | ||||||
|  |         /// <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) | ||||||
|  |         { | ||||||
|  |             _renderable = renderable; | ||||||
|  |             _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/> | ||||||
|  |         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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Matt Constable
					Matt Constable