فهرست منبع

Merge pull request #2344 from tznind/line-canvas-fix-offsets

Fixes 2343 - LineCanvas not respecting X and Y of clip bounds
Tig 2 سال پیش
والد
کامیت
3ecf42a61c
3فایلهای تغییر یافته به همراه155 افزوده شده و 119 حذف شده
  1. 75 99
      Terminal.Gui/Core/Graphs/LineCanvas.cs
  2. 13 2
      UICatalog/Scenarios/LineDrawing.cs
  3. 67 18
      UnitTests/Core/LineCanvasTests.cs

+ 75 - 99
Terminal.Gui/Core/Graphs/LineCanvas.cs

@@ -11,10 +11,10 @@ namespace Terminal.Gui.Graphs {
 	/// </summary>
 	public class LineCanvas {
 
-		
+
 		private List<StraightLine> lines = new List<StraightLine> ();
 
-		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> { 
+		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
 			{IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
 			{IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},
 			{IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()},
@@ -48,21 +48,22 @@ namespace Terminal.Gui.Graphs {
 		}
 		/// <summary>
 		/// Evaluate all currently defined lines that lie within 
-		/// <paramref name="inArea"/> and generate a 'bitmap' that
+		/// <paramref name="inArea"/> 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.
 		/// <returns></returns>
 		/// </summary>
 		/// <param name="inArea"></param>
-		/// <returns>Map as 2D array where first index is rows and second is column</returns>
-		public Rune? [,] GenerateImage (Rect inArea)
+		/// <returns>Mapping of all the points within <paramref name="inArea"/> to
+		/// line or intersection runes which should be drawn there.</returns>
+		public Dictionary<Point,Rune> GenerateImage (Rect inArea)
 		{
-			Rune? [,] canvas = new Rune? [inArea.Height, inArea.Width];
+			var map = new Dictionary<Point,Rune>();
 
 			// 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);
-
-				}
-			}
-
-			return canvas;
-		}
+					var rune = GetRuneForIntersects (Application.Driver, intersects);
 
-		/// <summary>
-		/// Draws all the lines that lie within the <paramref name="bounds"/> onto
-		/// the <paramref name="view"/> client area.  This method should be called from
-		/// <see cref="View.Redraw(Rect)"/>.
-		/// </summary>
-		/// <param name="view"></param>
-		/// <param name="bounds"></param>
-		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 
-		{
+		private class URIntersectionRuneResolver : IntersectionRuneResolver {
 
-			public URIntersectionRuneResolver() :
-				base('╮','╕','╖','╗','┐')
+			public URIntersectionRuneResolver () :
+				base ('╮', '╕', '╖', '╗', '┐')
 			{
-				
+
 			}
 		}
-		private class LLIntersectionRuneResolver : IntersectionRuneResolver 
-		{
+		private class LLIntersectionRuneResolver : IntersectionRuneResolver {
 
-			public LLIntersectionRuneResolver() :
-				base('╰','╘','╙','╚','└')
+			public LLIntersectionRuneResolver () :
+				base ('╰', '╘', '╙', '╚', '└')
 			{
-				
+
 			}
 		}
-		private class LRIntersectionRuneResolver : IntersectionRuneResolver 
-		{
-			public LRIntersectionRuneResolver() :
-				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;
 			}
 

+ 13 - 2
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)

+ 67 - 18
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.
 		/// </summary>
 		[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);
+		}
+
+
+		/// <summary>
+		/// Creates a new <see cref="View"/> into which a <see cref="LineCanvas"/> is rendered
+		/// at <see cref="View.DrawContentComplete"/> time.
+		/// </summary>
+		/// <param name="canvas">The <see cref="LineCanvas"/> you can draw into.</param>
+		/// <param name="offsetX">How far to offset drawing in X</param>
+		/// <param name="offsetY">How far to offset drawing in Y</param>
+		/// <returns></returns>
+		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;
 		}