mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-27 00:07:58 +01:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user