From 8261b25e5cc86c3ab8b3932dd482c87ddc603920 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Sat, 9 Jan 2021 18:34:07 +0100 Subject: [PATCH] Fix tree rendering Fixes some tree rendering problems where lines were not properly drawn at some levels during some circumstances. * Change the API back to only allow one root. * Now uses a stack based approach to rendering instead of recursion. * Removes the need for measuring the whole tree in advance. Leave this up to each child to render. --- docs/input/assets/images/tree.png | Bin 0 -> 24513 bytes docs/input/widgets/barchart.md | 2 +- docs/input/widgets/calendar.md | 2 +- docs/input/widgets/canvas-image.md | 2 +- docs/input/widgets/canvas.md | 2 +- docs/input/widgets/figlet.md | 2 +- docs/input/widgets/rule.md | 2 +- docs/input/widgets/tree.md | 70 ++++++ examples/Console/Prompt/Program.cs | 1 - examples/Console/Trees/Program.cs | 66 +++--- .../Tree/MultipleRoots.Output.verified.txt | 43 ---- .../Widgets/Tree/Render.Output.verified.txt | 41 ++++ ... => Render_NoChildren.Output.verified.txt} | 0 .../Tree/SingleRoot.Output.verified.txt | 40 ---- .../Spectre.Console.Tests.csproj | 4 + src/Spectre.Console.Tests/Unit/TreeTests.cs | 63 ++---- .../Extensions/HasTreeNodeExtensions.cs | 204 ++++++++++-------- .../Extensions/ListExtensions.cs | 38 ++++ .../Extensions/TreeExtensions.cs | 44 ++++ .../Extensions/TreeGuideExtensions.cs | 31 +++ .../Extensions/TreeNodeExtensions.cs | 47 ++++ src/Spectre.Console/IHasTreeNodes.cs | 8 +- .../Rendering/Tree/AsciiTreeAppearance.cs | 25 --- .../Rendering/Tree/AsciiTreeGuide.cs | 23 ++ .../Rendering/Tree/BoldLineTreeGuide.cs | 26 +++ .../Rendering/Tree/DoubleLineTreeGuide.cs | 26 +++ .../Rendering/Tree/LineTreeGuide.cs | 26 +++ .../{TreePart.cs => TreeGuidePart.cs} | 13 +- src/Spectre.Console/TreeAppearance.Known.cs | 15 -- src/Spectre.Console/TreeGuide.Known.cs | 30 +++ .../{TreeAppearance.cs => TreeGuide.cs} | 12 +- src/Spectre.Console/Widgets/BarChart.cs | 16 +- src/Spectre.Console/Widgets/Tree.cs | 178 +++++++-------- src/Spectre.Console/Widgets/TreeNode.cs | 41 ++-- 34 files changed, 697 insertions(+), 446 deletions(-) create mode 100644 docs/input/assets/images/tree.png create mode 100644 docs/input/widgets/tree.md delete mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Tree/MultipleRoots.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Tree/Render.Output.verified.txt rename src/Spectre.Console.Tests/Expectations/Widgets/Tree/{OnlyRoot.Output.verified.txt => Render_NoChildren.Output.verified.txt} (100%) delete mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Tree/SingleRoot.Output.verified.txt create mode 100644 src/Spectre.Console/Extensions/ListExtensions.cs create mode 100644 src/Spectre.Console/Extensions/TreeExtensions.cs create mode 100644 src/Spectre.Console/Extensions/TreeGuideExtensions.cs create mode 100644 src/Spectre.Console/Extensions/TreeNodeExtensions.cs delete mode 100644 src/Spectre.Console/Rendering/Tree/AsciiTreeAppearance.cs create mode 100644 src/Spectre.Console/Rendering/Tree/AsciiTreeGuide.cs create mode 100644 src/Spectre.Console/Rendering/Tree/BoldLineTreeGuide.cs create mode 100644 src/Spectre.Console/Rendering/Tree/DoubleLineTreeGuide.cs create mode 100644 src/Spectre.Console/Rendering/Tree/LineTreeGuide.cs rename src/Spectre.Console/Rendering/{TreePart.cs => TreeGuidePart.cs} (72%) delete mode 100644 src/Spectre.Console/TreeAppearance.Known.cs create mode 100644 src/Spectre.Console/TreeGuide.Known.cs rename src/Spectre.Console/{TreeAppearance.cs => TreeGuide.cs} (58%) diff --git a/docs/input/assets/images/tree.png b/docs/input/assets/images/tree.png new file mode 100644 index 0000000000000000000000000000000000000000..fe435fc95412f1a7afa7cbcca9e695410a048dd2 GIT binary patch literal 24513 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 + +# 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