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

Fixes 2343 - LineCanvas not respecting X and Y of clip bounds
This commit is contained in:
Tig
2023-02-20 22:17:17 +13:00
committed by GitHub
3 changed files with 158 additions and 122 deletions

View File

@@ -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);
var rune = GetRuneForIntersects (Application.Driver, intersects);
}
}
return canvas;
}
/// <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
{
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;
}

View File

@@ -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)

View File

@@ -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;
}