diff --git a/Terminal.Gui/Core/Graphs/LineCanvas.cs b/Terminal.Gui/Core/Graphs/LineCanvas.cs
new file mode 100644
index 000000000..04583518e
--- /dev/null
+++ b/Terminal.Gui/Core/Graphs/LineCanvas.cs
@@ -0,0 +1,514 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui.Graphs {
+
+
+ ///
+ /// Facilitates box drawing and line intersection detection
+ /// and rendering. Does not support diagonal lines.
+ ///
+ public class LineCanvas {
+
+
+ private List lines = new List ();
+
+ ///
+ /// Add a new line to the canvas starting at .
+ /// Use positive for Right and negative for Left
+ /// when is .
+ /// Use positive for Down and negative for Up
+ /// when is .
+ ///
+ /// Starting point.
+ /// Length of line. 0 for a dot.
+ /// Positive for Down/Right. Negative for Up/Left.
+ /// Direction of the line.
+ /// The style of line to use
+ public void AddLine (Point from, int length, Orientation orientation, BorderStyle style)
+ {
+ lines.Add (new StraightLine (from, length, orientation, style));
+ }
+ ///
+ /// Evaluate all currently defined lines that lie within
+ /// and generate a 'bitmap' 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)
+ {
+ Rune? [,] canvas = new Rune? [inArea.Height, inArea.Width];
+
+ // walk through each pixel of the bitmap
+ for (int y = 0; y < inArea.Height; y++) {
+ for (int x = 0; x < inArea.Width; x++) {
+
+ var intersects = lines
+ .Select (l => l.Intersects (x, y))
+ .Where (i => i != null)
+ .ToArray ();
+
+ // TODO: use Driver and LineStyle to map
+ canvas [y, x] = 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);
+ }
+ }
+ }
+ }
+
+ private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
+ {
+ if (!intersects.Any ())
+ return null;
+
+ var runeType = GetRuneTypeForIntersects (intersects);
+ var useDouble = intersects.Any (i => i.Line.Style == BorderStyle.Double && i.Line.Length != 0);
+ var useRounded = intersects.Any (i => i.Line.Style == BorderStyle.Rounded && i.Line.Length != 0);
+
+ switch (runeType) {
+ case IntersectionRuneType.None:
+ return null;
+ case IntersectionRuneType.Dot:
+ return (Rune)'.';
+ case IntersectionRuneType.ULCorner:
+ return useDouble ? driver.ULDCorner : useRounded ? driver.ULRCorner : driver.ULCorner;
+ case IntersectionRuneType.URCorner:
+ return useDouble ? driver.URDCorner : useRounded ? driver.URRCorner : driver.URCorner;
+ case IntersectionRuneType.LLCorner:
+ return useDouble ? driver.LLDCorner : useRounded ? driver.LLRCorner : driver.LLCorner;
+ case IntersectionRuneType.LRCorner:
+ return useDouble ? driver.LRDCorner : useRounded ? driver.LRRCorner : driver.LRCorner;
+ case IntersectionRuneType.TopTee:
+ return useDouble ? '╦' : driver.TopTee;
+ case IntersectionRuneType.BottomTee:
+ return useDouble ? '╩' : driver.BottomTee;
+ case IntersectionRuneType.RightTee:
+ return useDouble ? '╣' : driver.RightTee;
+ case IntersectionRuneType.LeftTee:
+ return useDouble ? '╠' : driver.LeftTee;
+ case IntersectionRuneType.Crosshair:
+ return useDouble ? '╬' : '┼';
+ case IntersectionRuneType.HLine:
+ return useDouble ? driver.HDLine : driver.HLine;
+ case IntersectionRuneType.VLine:
+ return useDouble ? driver.VDLine : driver.VLine;
+ default: throw new ArgumentOutOfRangeException (nameof (runeType));
+ }
+
+ }
+
+
+ private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects)
+ {
+ if(intersects.All(i=>i.Line.Length == 0)) {
+ return IntersectionRuneType.Dot;
+ }
+
+ // ignore dots
+ intersects = intersects.Where (i => i.Type != IntersectionType.Dot).ToArray ();
+
+ var set = new HashSet (intersects.Select (i => i.Type));
+
+ #region Crosshair Conditions
+ if (Has (set,
+ IntersectionType.PassOverHorizontal,
+ IntersectionType.PassOverVertical
+ )) {
+ return IntersectionRuneType.Crosshair;
+ }
+
+ if (Has (set,
+ IntersectionType.PassOverVertical,
+ IntersectionType.StartLeft,
+ IntersectionType.StartRight
+ )) {
+ return IntersectionRuneType.Crosshair;
+ }
+
+ if (Has (set,
+ IntersectionType.PassOverHorizontal,
+ IntersectionType.StartUp,
+ IntersectionType.StartDown
+ )) {
+ return IntersectionRuneType.Crosshair;
+ }
+
+
+ if (Has (set,
+ IntersectionType.StartLeft,
+ IntersectionType.StartRight,
+ IntersectionType.StartUp,
+ IntersectionType.StartDown)) {
+ return IntersectionRuneType.Crosshair;
+ }
+ #endregion
+
+
+ #region Corner Conditions
+ if (Exactly (set,
+ IntersectionType.StartRight,
+ IntersectionType.StartDown)) {
+ return IntersectionRuneType.ULCorner;
+ }
+
+ if (Exactly (set,
+ IntersectionType.StartLeft,
+ IntersectionType.StartDown)) {
+ return IntersectionRuneType.URCorner;
+ }
+
+ if (Exactly (set,
+ IntersectionType.StartUp,
+ IntersectionType.StartLeft)) {
+ return IntersectionRuneType.LRCorner;
+ }
+
+ if (Exactly (set,
+ IntersectionType.StartUp,
+ IntersectionType.StartRight)) {
+ return IntersectionRuneType.LLCorner;
+ }
+ #endregion Corner Conditions
+
+ #region T Conditions
+ if (Has (set,
+ IntersectionType.PassOverHorizontal,
+ IntersectionType.StartDown)) {
+ return IntersectionRuneType.TopTee;
+ }
+ if (Has (set,
+ IntersectionType.StartRight,
+ IntersectionType.StartLeft,
+ IntersectionType.StartDown)) {
+ return IntersectionRuneType.TopTee;
+ }
+
+ if (Has (set,
+ IntersectionType.PassOverHorizontal,
+ IntersectionType.StartUp)) {
+ return IntersectionRuneType.BottomTee;
+ }
+ if (Has (set,
+ IntersectionType.StartRight,
+ IntersectionType.StartLeft,
+ IntersectionType.StartUp)) {
+ return IntersectionRuneType.BottomTee;
+ }
+
+
+ if (Has (set,
+ IntersectionType.PassOverVertical,
+ IntersectionType.StartRight)) {
+ return IntersectionRuneType.LeftTee;
+ }
+ if (Has (set,
+ IntersectionType.StartRight,
+ IntersectionType.StartDown,
+ IntersectionType.StartUp)) {
+ return IntersectionRuneType.LeftTee;
+ }
+
+
+ if (Has (set,
+ IntersectionType.PassOverVertical,
+ IntersectionType.StartLeft)) {
+ return IntersectionRuneType.RightTee;
+ }
+ if (Has (set,
+ IntersectionType.StartLeft,
+ IntersectionType.StartDown,
+ IntersectionType.StartUp)) {
+ return IntersectionRuneType.RightTee;
+ }
+ #endregion
+
+ if (All (intersects, Orientation.Horizontal)) {
+ return IntersectionRuneType.HLine;
+ }
+
+ if (All (intersects, Orientation.Vertical)) {
+ return IntersectionRuneType.VLine;
+ }
+
+ return IntersectionRuneType.Dot;
+ }
+
+ private bool All (IntersectionDefinition [] intersects, Orientation orientation)
+ {
+ return intersects.All (i => i.Line.Orientation == orientation);
+ }
+
+ ///
+ /// Returns true if the collection has all the
+ /// specified (i.e. AND).
+ ///
+ ///
+ ///
+ ///
+ private bool Has (HashSet intersects, params IntersectionType [] types)
+ {
+ return types.All (t => intersects.Contains (t));
+ }
+
+ ///
+ /// Returns true if all requested appear in
+ /// and there are no additional
+ ///
+ ///
+ ///
+ ///
+ private bool Exactly (HashSet intersects, params IntersectionType [] types)
+ {
+ return intersects.SetEquals (types);
+ }
+
+ class IntersectionDefinition {
+ ///
+ /// The point at which the intersection happens
+ ///
+ public Point Point { get; }
+
+ ///
+ /// Defines how position relates
+ /// to .
+ ///
+ public IntersectionType Type { get; }
+
+ ///
+ /// The line that intersects
+ ///
+ public StraightLine Line { get; }
+
+ public IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
+ {
+ Point = point;
+ Type = type;
+ Line = line;
+ }
+ }
+
+ ///
+ /// The type of Rune that we will use before considering
+ /// double width, curved borders etc
+ ///
+ enum IntersectionRuneType {
+ None,
+ Dot,
+ ULCorner,
+ URCorner,
+ LLCorner,
+ LRCorner,
+ TopTee,
+ BottomTee,
+ RightTee,
+ LeftTee,
+ Crosshair,
+ HLine,
+ VLine,
+ }
+
+ enum IntersectionType {
+ ///
+ /// There is no intersection
+ ///
+ None,
+
+ ///
+ /// A line passes directly over this point traveling along
+ /// the horizontal axis
+ ///
+ PassOverHorizontal,
+
+ ///
+ /// A line passes directly over this point traveling along
+ /// the vertical axis
+ ///
+ PassOverVertical,
+
+ ///
+ /// A line starts at this point and is traveling up
+ ///
+ StartUp,
+
+ ///
+ /// A line starts at this point and is traveling right
+ ///
+ StartRight,
+
+ ///
+ /// A line starts at this point and is traveling down
+ ///
+ StartDown,
+
+ ///
+ /// A line starts at this point and is traveling left
+ ///
+ StartLeft,
+
+ ///
+ /// A line exists at this point who has 0 length
+ ///
+ Dot
+ }
+
+ class StraightLine {
+ public Point Start { get; }
+ public int Length { get; }
+ public Orientation Orientation { get; }
+ public BorderStyle Style { get; }
+
+ public StraightLine (Point start, int length, Orientation orientation, BorderStyle style)
+ {
+ this.Start = start;
+ this.Length = length;
+ this.Orientation = orientation;
+ this.Style = style;
+ }
+
+ internal IntersectionDefinition Intersects (int x, int y)
+ {
+ if (IsDot ()) {
+ if (StartsAt (x, y)) {
+ return new IntersectionDefinition (Start, IntersectionType.Dot, this);
+ } else {
+ return null;
+ }
+ }
+
+ switch (Orientation) {
+ case Orientation.Horizontal: return IntersectsHorizontally (x, y);
+ case Orientation.Vertical: return IntersectsVertically (x, y);
+ default: throw new ArgumentOutOfRangeException (nameof (Orientation));
+ }
+
+ }
+
+ private IntersectionDefinition IntersectsHorizontally (int x, int y)
+ {
+ if (Start.Y != y) {
+ return null;
+ } else {
+ if (StartsAt (x, y)) {
+
+ return new IntersectionDefinition (
+ Start,
+ Length < 0 ? IntersectionType.StartLeft : IntersectionType.StartRight,
+ this
+ );
+
+ }
+
+ if (EndsAt (x, y)) {
+
+ return new IntersectionDefinition (
+ Start,
+ Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
+ this
+ );
+
+ } else {
+ var xmin = Math.Min (Start.X, Start.X + Length);
+ var xmax = Math.Max (Start.X, Start.X + Length);
+
+ if (xmin < x && xmax > x) {
+ return new IntersectionDefinition (
+ new Point (x, y),
+ IntersectionType.PassOverHorizontal,
+ this
+ );
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private IntersectionDefinition IntersectsVertically (int x, int y)
+ {
+ if (Start.X != x) {
+ return null;
+ } else {
+ if (StartsAt (x, y)) {
+
+ return new IntersectionDefinition (
+ Start,
+ Length < 0 ? IntersectionType.StartUp : IntersectionType.StartDown,
+ this
+ );
+
+ }
+
+ if (EndsAt (x, y)) {
+
+ return new IntersectionDefinition (
+ Start,
+ Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
+ this
+ );
+
+ } else {
+ var ymin = Math.Min (Start.Y, Start.Y + Length);
+ var ymax = Math.Max (Start.Y, Start.Y + Length);
+
+ if (ymin < y && ymax > y) {
+ return new IntersectionDefinition (
+ new Point (x, y),
+ IntersectionType.PassOverVertical,
+ this
+ );
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private bool EndsAt (int x, int y)
+ {
+ if (Orientation == Orientation.Horizontal) {
+ return Start.X + Length == x && Start.Y == y;
+ }
+
+ return Start.X == x && Start.Y + Length == y;
+ }
+
+ private bool StartsAt (int x, int y)
+ {
+ return Start.X == x && Start.Y == y;
+ }
+
+ private bool IsDot ()
+ {
+ return Length == 0;
+ }
+ }
+ }
+}
diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs
new file mode 100644
index 000000000..f3adac86d
--- /dev/null
+++ b/UICatalog/Scenarios/LineDrawing.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Metadata.Ecma335;
+using Terminal.Gui;
+using Terminal.Gui.Graphs;
+
+namespace UICatalog.Scenarios {
+
+ [ScenarioMetadata (Name: "Line Drawing", Description: "Demonstrates LineCanvas.")]
+ [ScenarioCategory ("Controls")]
+ [ScenarioCategory ("Layout")]
+ public class LineDrawing : Scenario {
+
+ public override void Setup ()
+ {
+ var toolsWidth = 8;
+
+ var canvas = new DrawingArea {
+ Width = Dim.Fill (-toolsWidth),
+ Height = Dim.Fill ()
+ };
+
+ var tools = new ToolsView (toolsWidth) {
+ Y = 1,
+ X = Pos.AnchorEnd (toolsWidth + 1),
+ Height = Dim.Fill (),
+ Width = Dim.Fill ()
+ };
+
+
+ tools.ColorChanged += (c) => canvas.SetColor (c);
+ tools.SetStyle += (b) => canvas.BorderStyle = b;
+
+ Win.Add (canvas);
+ Win.Add (tools);
+ Win.Add (new Label (" -Tools-") { X = Pos.AnchorEnd (toolsWidth + 1) });
+ }
+
+ class ToolsView : View {
+
+ LineCanvas grid;
+ public event Action ColorChanged;
+ public event Action SetStyle;
+
+ Dictionary swatches = new Dictionary {
+ { new Point(1,1),Color.Red},
+ { new Point(3,1),Color.Green},
+ { new Point(5,1),Color.BrightBlue},
+ { new Point(7,1),Color.Black},
+ { new Point(1,3),Color.White},
+ };
+
+ public ToolsView (int width)
+ {
+ grid = new LineCanvas ();
+
+ grid.AddLine (new Point (0, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+ grid.AddLine (new Point (0, 0), width, Orientation.Horizontal, BorderStyle.Single);
+ grid.AddLine (new Point (width, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+
+ grid.AddLine (new Point (0, 2), width, Orientation.Horizontal, BorderStyle.Single);
+
+ grid.AddLine (new Point (2, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+ grid.AddLine (new Point (4, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+ grid.AddLine (new Point (6, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+
+ grid.AddLine (new Point (0, 4), width, Orientation.Horizontal, BorderStyle.Single);
+ }
+ public override void Redraw (Rect bounds)
+ {
+ base.Redraw (bounds);
+
+ Driver.SetAttribute (new Terminal.Gui.Attribute (Color.DarkGray, ColorScheme.Normal.Background));
+ grid.Draw (this, bounds);
+
+ foreach (var swatch in swatches) {
+ Driver.SetAttribute (new Terminal.Gui.Attribute (swatch.Value, ColorScheme.Normal.Background));
+ AddRune (swatch.Key.X, swatch.Key.Y, '█');
+ }
+
+ Driver.SetAttribute (new Terminal.Gui.Attribute (ColorScheme.Normal.Foreground, ColorScheme.Normal.Background));
+ AddRune (3, 3, Application.Driver.HDLine);
+ AddRune (5, 3, Application.Driver.HLine);
+ AddRune (7, 3, Application.Driver.ULRCorner);
+ }
+
+ public override bool OnMouseEvent (MouseEvent mouseEvent)
+ {
+ if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+ foreach (var swatch in swatches) {
+ if (mouseEvent.X == swatch.Key.X && mouseEvent.Y == swatch.Key.Y) {
+
+ ColorChanged?.Invoke (swatch.Value);
+ return true;
+ }
+ }
+
+ if (mouseEvent.X == 3 && mouseEvent.Y == 3) {
+
+ SetStyle?.Invoke (BorderStyle.Double);
+ return true;
+ }
+ if (mouseEvent.X == 5 && mouseEvent.Y == 3) {
+
+ SetStyle?.Invoke (BorderStyle.Single);
+ return true;
+ }
+ if (mouseEvent.X == 7 && mouseEvent.Y == 3) {
+
+ SetStyle?.Invoke (BorderStyle.Rounded);
+ return true;
+ }
+ }
+
+ return base.OnMouseEvent (mouseEvent);
+ }
+ }
+
+ class DrawingArea : View {
+ ///
+ /// Index into by color.
+ ///
+ Dictionary colorLayers = new Dictionary ();
+ List canvases = new List ();
+ int currentColor;
+
+ Point? currentLineStart = null;
+
+ public BorderStyle BorderStyle { get; internal set; }
+
+ public DrawingArea ()
+ {
+ AddCanvas (Color.White);
+ }
+
+ private void AddCanvas (Color c)
+ {
+ if (colorLayers.ContainsKey (c)) {
+ return;
+ }
+
+ canvases.Add (new LineCanvas ());
+ colorLayers.Add (c, canvases.Count - 1);
+ currentColor = canvases.Count - 1;
+ }
+
+ public override void Redraw (Rect bounds)
+ {
+ base.Redraw (bounds);
+
+ foreach (var kvp in colorLayers) {
+
+ Driver.SetAttribute (new Terminal.Gui.Attribute (kvp.Key, ColorScheme.Normal.Background));
+ canvases [kvp.Value].Draw (this, bounds);
+ }
+ }
+ public override bool OnMouseEvent (MouseEvent mouseEvent)
+ {
+
+ if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) {
+ if (currentLineStart == null) {
+ currentLineStart = new Point (mouseEvent.X, mouseEvent.Y);
+ }
+ } else {
+ if (currentLineStart != null) {
+
+ var start = currentLineStart.Value;
+ var end = new Point (mouseEvent.X, mouseEvent.Y);
+ var orientation = Orientation.Vertical;
+ var length = end.Y - start.Y;
+
+ // if line is wider than it is tall switch to horizontal
+ if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y)) {
+ orientation = Orientation.Horizontal;
+ length = end.X - start.X;
+ }
+
+
+ canvases [currentColor].AddLine (
+ start,
+ length,
+ orientation,
+ BorderStyle);
+
+ currentLineStart = null;
+ SetNeedsDisplay ();
+ }
+ }
+
+ return base.OnMouseEvent (mouseEvent);
+ }
+
+ internal void SetColor (Color c)
+ {
+ AddCanvas (c);
+ currentColor = colorLayers [c];
+ }
+
+ }
+ }
+}
diff --git a/UnitTests/LineCanvasTests.cs b/UnitTests/LineCanvasTests.cs
new file mode 100644
index 000000000..35c657361
--- /dev/null
+++ b/UnitTests/LineCanvasTests.cs
@@ -0,0 +1,235 @@
+using Terminal.Gui.Graphs;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.Core {
+ public class LineCanvasTests {
+
+ readonly ITestOutputHelper output;
+
+ public LineCanvasTests (ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Dot ()
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 0, Orientation.Horizontal, BorderStyle.Single);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+.";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ [InlineData (BorderStyle.Single)]
+ [InlineData (BorderStyle.Rounded)]
+ [Theory, AutoInitShutdown]
+ public void TestLineCanvas_Horizontal (BorderStyle style)
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, style);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+──";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Horizontal_Double ()
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Double);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+══";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ [InlineData (BorderStyle.Single)]
+ [InlineData(BorderStyle.Rounded)]
+ [Theory, AutoInitShutdown]
+ public void TestLineCanvas_Vertical (BorderStyle style)
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Vertical, style);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+│
+│";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Vertical_Double ()
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Vertical, BorderStyle.Double);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+║
+║";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ ///
+ /// This test demonstrates that corners are only drawn when lines overlap.
+ /// Not when they terminate adjacent to one another.
+ ///
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Corner_NoOverlap()
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single);
+ canvas.AddLine (new Point (0, 1), 1, Orientation.Vertical, BorderStyle.Single);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+──
+│
+│";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+ ///
+ /// This test demonstrates how to correctly trigger a corner. By
+ /// overlapping the lines in the same cell
+ ///
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Corner_Correct ()
+ {
+ var v = GetCanvas (out var canvas);
+ canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single);
+ canvas.AddLine (new Point (0, 0), 2, Orientation.Vertical, BorderStyle.Single);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+┌─
+│
+│";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+
+ }
+ [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);
+ canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Single);
+ canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single);
+
+
+ canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Single);
+ canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Single);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+┌────┬───┐
+│ │ │
+├────┼───┤
+│ │ │
+└────┴───┘";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ ///
+ /// Demonstrates when corners are used. Notice how
+ /// not all lines declare rounded. If there are 1+ lines intersecting and a corner is
+ /// to be used then if any of them are rounded a rounded corner is used.
+ ///
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Window_Rounded ()
+ {
+ var v = GetCanvas (out var canvas);
+
+ // 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, 4), -9, Orientation.Horizontal, BorderStyle.Rounded);
+ canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single);
+
+ // These lines say rounded but they will result in the T sections which are never rounded.
+ canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Rounded);
+ canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Rounded);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+╭────┬───╮
+│ │ │
+├────┼───┤
+│ │ │
+╰────┴───╯";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ [Fact, AutoInitShutdown]
+ public void TestLineCanvas_Window_Double ()
+ {
+ var v = GetCanvas (out var canvas);
+
+ // outer box
+ canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Double);
+ canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Double);
+ canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Double);
+ canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double);
+
+
+ canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Double);
+ canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Double);
+
+ v.Redraw (v.Bounds);
+
+ string looksLike =
+@"
+╔════╦═══╗
+║ ║ ║
+╠════╬═══╣
+║ ║ ║
+╚════╩═══╝";
+ TestHelpers.AssertDriverContentsAre (looksLike, output);
+ }
+
+ private View GetCanvas (out LineCanvas canvas)
+ {
+ var v = new View {
+ Width = 10,
+ Height = 5,
+ Bounds = new Rect (0, 0, 10, 5)
+ };
+
+ var canvasCopy = canvas = new LineCanvas ();
+ v.DrawContentComplete += (r)=> canvasCopy.Draw (v, v.Bounds);
+
+ return v;
+ }
+ }
+}