diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs index ab45c0264..1b1cbe76b 100644 --- a/Terminal.Gui/Core/Graphs/LineCanvas.cs +++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs @@ -11,10 +11,10 @@ namespace Terminal.Gui.Graphs { /// public class LineCanvas { - + private List lines = new List (); - Dictionary runeResolvers = new Dictionary { + Dictionary runeResolvers = new Dictionary { {IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()}, {IntersectionRuneType.URCorner,new URIntersectionRuneResolver()}, {IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()}, @@ -48,21 +48,22 @@ namespace Terminal.Gui.Graphs { } /// /// Evaluate all currently defined lines that lie within - /// and generate a 'bitmap' that + /// and map that /// shows what characters (if any) should be rendered at each /// point so that all lines connect up correctly with appropriate /// intersection symbols. /// /// /// - /// Map as 2D array where first index is rows and second is column - public Rune? [,] GenerateImage (Rect inArea) + /// Mapping of all the points within to + /// line or intersection runes which should be drawn there. + public Dictionary GenerateImage (Rect inArea) { - Rune? [,] canvas = new Rune? [inArea.Height, inArea.Width]; + var map = new Dictionary(); // walk through each pixel of the bitmap - for (int y = 0; y < inArea.Height; y++) { - for (int x = 0; x < inArea.Width; x++) { + for (int y = inArea.Y; y < inArea.Height; y++) { + for (int x = inArea.X; x < inArea.Width; x++) { var intersects = lines .Select (l => l.Intersects (x, y)) @@ -70,45 +71,26 @@ namespace Terminal.Gui.Graphs { .ToArray (); // TODO: use Driver and LineStyle to map - canvas [y, x] = GetRuneForIntersects (Application.Driver, intersects); + var rune = GetRuneForIntersects (Application.Driver, intersects); - } - } - - return canvas; - } - - /// - /// Draws all the lines that lie within the onto - /// the client area. This method should be called from - /// . - /// - /// - /// - public void Draw (View view, Rect bounds) - { - var runes = GenerateImage (bounds); - - for (int y = bounds.Y; y < bounds.Height; y++) { - for (int x = bounds.X; x < bounds.Width; x++) { - var rune = runes [y, x]; - - if (rune.HasValue) { - view.AddRune (x, y, rune.Value); + if(rune != null) + { + map.Add(new Point(x,y),rune.Value); } } } + + return map; } - private abstract class IntersectionRuneResolver - { + private abstract class IntersectionRuneResolver { readonly Rune round; readonly Rune doubleH; readonly Rune doubleV; readonly Rune doubleBoth; readonly Rune normal; - public IntersectionRuneResolver(Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune normal) + public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune normal) { this.round = round; this.doubleH = doubleH; @@ -121,17 +103,15 @@ namespace Terminal.Gui.Graphs { { var useRounded = intersects.Any (i => i.Line.Style == BorderStyle.Rounded && i.Line.Length != 0); - bool doubleHorizontal = intersects.Any(l=>l.Line.Orientation == Orientation.Horizontal && l.Line.Style == BorderStyle.Double); - bool doubleVertical = intersects.Any(l=>l.Line.Orientation == Orientation.Vertical && l.Line.Style == BorderStyle.Double); + bool doubleHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && l.Line.Style == BorderStyle.Double); + bool doubleVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && l.Line.Style == BorderStyle.Double); - if(doubleHorizontal) - { - return doubleVertical ? doubleBoth : doubleH; + if (doubleHorizontal) { + return doubleVertical ? doubleBoth : doubleH; } - - if(doubleVertical) - { + + if (doubleVertical) { return doubleV; } @@ -139,75 +119,71 @@ namespace Terminal.Gui.Graphs { } } - private class ULIntersectionRuneResolver : IntersectionRuneResolver - { - public ULIntersectionRuneResolver() : - base('╭','╒','╓','╔','┌') + private class ULIntersectionRuneResolver : IntersectionRuneResolver { + public ULIntersectionRuneResolver () : + base ('╭', '╒', '╓', '╔', '┌') { - - } - } - private class URIntersectionRuneResolver : IntersectionRuneResolver - { - public URIntersectionRuneResolver() : - base('╮','╕','╖','╗','┐') - { - } } - private class LLIntersectionRuneResolver : IntersectionRuneResolver - { + private class URIntersectionRuneResolver : IntersectionRuneResolver { - public LLIntersectionRuneResolver() : - base('╰','╘','╙','╚','└') + public URIntersectionRuneResolver () : + base ('╮', '╕', '╖', '╗', '┐') { - + } } - private class LRIntersectionRuneResolver : IntersectionRuneResolver - { - public LRIntersectionRuneResolver() : - base('╯','╛','╜','╝','┘') + private class LLIntersectionRuneResolver : IntersectionRuneResolver { + + public LLIntersectionRuneResolver () : + base ('╰', '╘', '╙', '╚', '└') { - + + } + } + private class LRIntersectionRuneResolver : IntersectionRuneResolver { + public LRIntersectionRuneResolver () : + base ('╯', '╛', '╜', '╝', '┘') + { + } } - private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public TopTeeIntersectionRuneResolver(): - base('┬','╤','╥','╦','┬'){ - - } + private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver { + public TopTeeIntersectionRuneResolver () : + base ('┬', '╤', '╥', '╦', '┬') + { + + } } - private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public LeftTeeIntersectionRuneResolver(): - base('├','╞','╟','╠','├'){ - - } + private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver { + public LeftTeeIntersectionRuneResolver () : + base ('├', '╞', '╟', '╠', '├') + { + + } } - private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public RightTeeIntersectionRuneResolver(): - base('┤','╡','╢','╣','┤'){ - - } + private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver { + public RightTeeIntersectionRuneResolver () : + base ('┤', '╡', '╢', '╣', '┤') + { + + } } - private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver - { - public BottomTeeIntersectionRuneResolver(): - base('┴','╧','╨','╩','┴'){ - - } + private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver { + public BottomTeeIntersectionRuneResolver () : + base ('┴', '╧', '╨', '╩', '┴') + { + + } } - private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver - { - public CrosshairIntersectionRuneResolver(): - base('┼','╪','╫','╬','┼'){ - - } + private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver { + public CrosshairIntersectionRuneResolver () : + base ('┼', '╪', '╫', '╬', '┼') + { + + } } private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects) @@ -217,7 +193,7 @@ namespace Terminal.Gui.Graphs { var runeType = GetRuneTypeForIntersects (intersects); - if(runeResolvers.ContainsKey (runeType)) { + if (runeResolvers.ContainsKey (runeType)) { return runeResolvers [runeType].GetRuneForIntersects (driver, intersects); } @@ -228,13 +204,13 @@ namespace Terminal.Gui.Graphs { // TODO: maybe make these resolvers to for simplicity? // or for dotted lines later on or that kind of thing? switch (runeType) { - case IntersectionRuneType.None: + case IntersectionRuneType.None: return null; - case IntersectionRuneType.Dot: + case IntersectionRuneType.Dot: return (Rune)'.'; - case IntersectionRuneType.HLine: + case IntersectionRuneType.HLine: return useDouble ? driver.HDLine : driver.HLine; - case IntersectionRuneType.VLine: + case IntersectionRuneType.VLine: return useDouble ? driver.VDLine : driver.VLine; default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType); } @@ -243,7 +219,7 @@ namespace Terminal.Gui.Graphs { private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects) { - if(intersects.All(i=>i.Line.Length == 0)) { + if (intersects.All (i => i.Line.Length == 0)) { return IntersectionRuneType.Dot; } diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs index f3adac86d..f5670c0c4 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -71,7 +71,12 @@ namespace UICatalog.Scenarios { base.Redraw (bounds); Driver.SetAttribute (new Terminal.Gui.Attribute (Color.DarkGray, ColorScheme.Normal.Background)); - grid.Draw (this, bounds); + + + foreach(var p in grid.GenerateImage(bounds)) + { + this.AddRune(p.Key.X,p.Key.Y,p.Value); + } foreach (var swatch in swatches) { Driver.SetAttribute (new Terminal.Gui.Attribute (swatch.Value, ColorScheme.Normal.Background)); @@ -151,7 +156,13 @@ namespace UICatalog.Scenarios { foreach (var kvp in colorLayers) { Driver.SetAttribute (new Terminal.Gui.Attribute (kvp.Key, ColorScheme.Normal.Background)); - canvases [kvp.Value].Draw (this, bounds); + + var canvas = canvases [kvp.Value]; + + foreach(var p in canvas.GenerateImage(bounds)) + { + this.AddRune(p.Key.X,p.Key.Y,p.Value); + } } } public override bool OnMouseEvent (MouseEvent mouseEvent) diff --git a/UnitTests/Core/LineCanvasTests.cs b/UnitTests/Core/LineCanvasTests.cs index 7e988b0de..65bc1f3a2 100644 --- a/UnitTests/Core/LineCanvasTests.cs +++ b/UnitTests/Core/LineCanvasTests.cs @@ -1,4 +1,7 @@ -using Terminal.Gui.Graphs; +using System; +using System.Collections.Generic; +using System.Text; +using Terminal.Gui.Graphs; using Xunit; using Xunit.Abstractions; @@ -57,7 +60,7 @@ namespace Terminal.Gui.CoreTests { } [InlineData (BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Rounded)] [Theory, AutoInitShutdown] public void TestLineCanvas_Vertical (BorderStyle style) { @@ -93,7 +96,7 @@ namespace Terminal.Gui.CoreTests { /// Not when they terminate adjacent to one another. /// [Fact, AutoInitShutdown] - public void TestLineCanvas_Corner_NoOverlap() + public void TestLineCanvas_Corner_NoOverlap () { var v = GetCanvas (out var canvas); canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single); @@ -127,13 +130,14 @@ namespace Terminal.Gui.CoreTests { │ │"; TestHelpers.AssertDriverContentsAre (looksLike, output); - + } - [Fact,AutoInitShutdown] + + [Fact, AutoInitShutdown] public void TestLineCanvas_Window () { var v = GetCanvas (out var canvas); - + // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Single); canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); @@ -168,10 +172,10 @@ namespace Terminal.Gui.CoreTests { // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Rounded); - + // BorderStyle.Single is ignored because corner overlaps with the above line which is Rounded // this results in a rounded corner being used. - canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single); canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Rounded); canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single); @@ -220,8 +224,8 @@ namespace Terminal.Gui.CoreTests { [Theory, AutoInitShutdown] - [InlineData(BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Single)] + [InlineData (BorderStyle.Rounded)] public void TestLineCanvas_Window_DoubleTop_SingleSides (BorderStyle thinStyle) { var v = GetCanvas (out var canvas); @@ -233,7 +237,7 @@ namespace Terminal.Gui.CoreTests { canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, thinStyle); - canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical,thinStyle); + canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, thinStyle); canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Double); v.Redraw (v.Bounds); @@ -250,8 +254,8 @@ namespace Terminal.Gui.CoreTests { } [Theory, AutoInitShutdown] - [InlineData(BorderStyle.Single)] - [InlineData(BorderStyle.Rounded)] + [InlineData (BorderStyle.Single)] + [InlineData (BorderStyle.Rounded)] public void TestLineCanvas_Window_SingleTop_DoubleSides (BorderStyle thinStyle) { var v = GetCanvas (out var canvas); @@ -259,8 +263,8 @@ namespace Terminal.Gui.CoreTests { // outer box canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, thinStyle); canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Double); - canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal,thinStyle); - canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double); + canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, thinStyle); + canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double); canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Double); @@ -280,7 +284,44 @@ namespace Terminal.Gui.CoreTests { TestHelpers.AssertDriverContentsAre (looksLike, output); } - private View GetCanvas (out LineCanvas canvas) + [Fact, AutoInitShutdown] + public void TestLineCanvas_LeaveMargin_Top1_Left1 () + { + // Draw at 1,1 within client area of View (i.e. leave a top and left margin of 1) + var v = GetCanvas (out var canvas, 1, 1); + + // outer box + canvas.AddLine (new Point (0, 0), 8, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (8, 0), 3, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (8, 3), -8, Orientation.Horizontal, BorderStyle.Single); + canvas.AddLine (new Point (0, 3), -3, Orientation.Vertical, BorderStyle.Single); + + + canvas.AddLine (new Point (5, 0), 3, Orientation.Vertical, BorderStyle.Single); + canvas.AddLine (new Point (0, 2), 8, Orientation.Horizontal, BorderStyle.Single); + + v.Redraw (v.Bounds); + + string looksLike = +@" + ┌────┬──┐ + │ │ │ + ├────┼──┤ + └────┴──┘ +"; + TestHelpers.AssertDriverContentsAre (looksLike, output); + } + + + /// + /// Creates a new into which a is rendered + /// at time. + /// + /// The you can draw into. + /// How far to offset drawing in X + /// How far to offset drawing in Y + /// + private View GetCanvas (out LineCanvas canvas, int offsetX = 0, int offsetY = 0) { var v = new View { Width = 10, @@ -288,8 +329,16 @@ namespace Terminal.Gui.CoreTests { Bounds = new Rect (0, 0, 10, 5) }; - var canvasCopy = canvas = new LineCanvas (); - v.DrawContentComplete += (r)=> canvasCopy.Draw (v, v.Bounds); + var canvasCopy = canvas = new LineCanvas (); + v.DrawContentComplete += (r) => { + foreach(var p in canvasCopy.GenerateImage(v.Bounds)) + { + v.AddRune( + offsetX + p.Key.X, + offsetY + p.Key.Y, + p.Value); + } + }; return v; }