Add DrawIncompleteFrame method and unit tests.

This commit is contained in:
BDisp
2023-11-18 23:22:46 +00:00
parent 98f9e1e856
commit 680ba264e1
2 changed files with 580 additions and 13 deletions

View File

@@ -4,6 +4,28 @@ using System.Text;
namespace Terminal.Gui {
public partial class View {
/// <summary>
/// Specifies the side to start when draw frame with
/// <see cref="DrawIncompleteFrame(ValueTuple{int, Side}, ValueTuple{int, Side}, Rect, LineStyle, Attribute?, bool)"/> method.
/// </summary>
public enum Side {
/// <summary>
/// Start on left.
/// </summary>
Left,
/// <summary>
/// Start on top.
/// </summary>
Top,
/// <summary>
/// Start on right.
/// </summary>
Right,
/// <summary>
/// Start on bottom.
/// </summary>
Bottom
};
ColorScheme _colorScheme;
@@ -184,7 +206,7 @@ namespace Terminal.Gui {
/// This clears the Bounds used by this view.
/// </para>
/// </remarks>
public void Clear () => Clear (ViewToScreen(Bounds));
public void Clear () => Clear (ViewToScreen (Bounds));
// BUGBUG: This version of the Clear API should be removed. We should have a tenet that says
// "View APIs only deal with View-relative coords". This is only used by ComboBox which can
@@ -504,24 +526,257 @@ namespace Terminal.Gui {
}
/// <summary>
/// Draw a frame based on the passed bounds to the screen relative.
/// Draws a rectangular frame. The frame will be merged (auto-joined) with any other lines drawn by this View
/// if <paramref name="mergeWithLineCanvas"/> is true, otherwise will be rendered immediately.
/// </summary>
/// <param name="bounds">The bounds view relative.</param>
/// <param name="rect">The view relative location and size of the frame.</param>
/// <param name="lineStyle">The line style.</param>
/// <param name="attribute">The color to use.</param>
public void DrawFrame (Rect bounds, LineStyle lineStyle, Attribute? attribute = null)
/// <param name="attribute">The colors to be used.</param>
/// <param name="mergeWithLineCanvas">When drawing the frame, allow it to integrate (join) to other frames in other controls.
/// Or false to simply draw the rect exactly with no side effects.</param>
public void DrawFrame (Rect rect, LineStyle lineStyle, Attribute? attribute = null, bool mergeWithLineCanvas = true)
{
var vts = ViewToScreen (bounds);
LineCanvas.AddLine (new Point (vts.X, vts.Y), vts.Width,
LineCanvas lc;
if (mergeWithLineCanvas) {
lc = new LineCanvas ();
} else {
lc = LineCanvas;
}
var vts = ViewToScreen (rect);
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
LineCanvas.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
LineCanvas.AddLine (new Point (vts.X, vts.Bottom - 1), vts.Width,
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
LineCanvas.AddLine (new Point (vts.X, vts.Y), vts.Height,
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
OnRenderLineCanvas ();
if (mergeWithLineCanvas) {
LineCanvas.Merge (lc);
} else {
OnRenderLineCanvas ();
}
}
/// <summary>
/// Draws an incomplete frame. The frame will be merged (auto-joined) with any other lines drawn by this View
/// if <paramref name="mergeWithLineCanvas"/> is true, otherwise will be rendered immediately.
/// The frame is always drawn clockwise. For <see cref="Side.Top"/> and <see cref="Side.Right"/> the end position must
/// be greater or equal to the start and for <see cref="Side.Left"/> and <see cref="Side.Bottom"/> the end position must
/// be less or equal to the start.
/// </summary>
/// <param name="startPos">The start and side position screen relative.</param>
/// <param name="endPos">The end and side position screen relative.</param>
/// <param name="rect">The view relative location and size of the frame.</param>
/// <param name="lineStyle">The line style.</param>
/// <param name="attribute">The colors to be used.</param>
/// <param name="mergeWithLineCanvas">When drawing the frame, allow it to integrate (join) to other frames in other controls.
/// Or false to simply draw the rect exactly with no side effects.</param>
public void DrawIncompleteFrame ((int start, Side side) startPos, (int end, Side side) endPos, Rect rect, LineStyle lineStyle, Attribute? attribute = null, bool mergeWithLineCanvas = true)
{
var vts = ViewToScreen (rect);
LineCanvas lc;
if (mergeWithLineCanvas) {
lc = new LineCanvas ();
} else {
lc = LineCanvas;
}
var start = startPos.start;
var end = endPos.end;
switch (startPos.side) {
case Side.Left:
if (start == vts.Y) {
lc.AddLine (new Point (vts.X, start), 1,
Orientation.Vertical, lineStyle, attribute);
} else {
if (end <= start && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.X, start), end - start - 1,
Orientation.Vertical, lineStyle, attribute);
break;
} else {
lc.AddLine (new Point (vts.X, start), vts.Y - start - 1,
Orientation.Vertical, lineStyle, attribute);
}
}
switch (endPos.side) {
case Side.Left:
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
if (end <= vts.Bottom - 1 && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -(vts.Bottom - end),
Orientation.Vertical, lineStyle, attribute);
}
break;
case Side.Top:
lc.AddLine (new Point (vts.X, vts.Y), end,
Orientation.Horizontal, lineStyle, attribute);
break;
case Side.Right:
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Y), end + 1,
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Bottom:
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -end,
Orientation.Horizontal, lineStyle, attribute);
break;
}
break;
case Side.Top:
if (start == vts.Width - 1) {
lc.AddLine (new Point (vts.X + start, vts.Y), -1,
Orientation.Horizontal, lineStyle, attribute);
} else if (end >= start && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.X + start, vts.Y), end - start + 1,
Orientation.Horizontal, lineStyle, attribute);
break;
} else if (vts.Width - start > 0) {
lc.AddLine (new Point (vts.X + start, vts.Y), Math.Max (vts.Width - start, 0),
Orientation.Horizontal, lineStyle, attribute);
}
switch (endPos.side) {
case Side.Left:
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -end,
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Top:
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
if (end >= 0 && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.X, vts.Y), end + 1,
Orientation.Horizontal, lineStyle, attribute);
}
break;
case Side.Right:
lc.AddLine (new Point (vts.Right - 1, vts.Y), end,
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Bottom:
lc.AddLine (new Point (vts.Right - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.Right - 1, vts.Bottom - 1), -(vts.Width - end),
Orientation.Horizontal, lineStyle, attribute);
break;
}
break;
case Side.Right:
if (start == vts.Bottom - 1) {
lc.AddLine (new Point (vts.Width - 1, start), -1,
Orientation.Vertical, lineStyle, attribute);
} else {
if (end >= start && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.Width - 1, start), end - start + 1,
Orientation.Vertical, lineStyle, attribute);
break;
} else {
lc.AddLine (new Point (vts.Width - 1, start), vts.Bottom - start,
Orientation.Vertical, lineStyle, attribute);
}
}
switch (endPos.side) {
case Side.Left:
lc.AddLine (new Point (vts.Width - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -(vts.Bottom - end),
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Top:
lc.AddLine (new Point (vts.Width - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Y), end,
Orientation.Horizontal, lineStyle, attribute);
break;
case Side.Right:
lc.AddLine (new Point (vts.Width - 1, vts.Bottom - 1), -vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
if (end >= 0 && end < vts.Bottom - 1 && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.Width - 1, vts.Y), end + 1,
Orientation.Vertical, lineStyle, attribute);
}
break;
case Side.Bottom:
lc.AddLine (new Point (vts.Width - 1, vts.Bottom - 1), -(vts.Width - end),
Orientation.Horizontal, lineStyle, attribute);
break;
}
break;
case Side.Bottom:
if (start == vts.X) {
lc.AddLine (new Point (vts.X, vts.Bottom - 1), 1,
Orientation.Horizontal, lineStyle, attribute);
} else if (end <= start && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.X + start, vts.Bottom - 1), -(start - end + 1),
Orientation.Horizontal, lineStyle, attribute);
break;
} else {
lc.AddLine (new Point (vts.X + start, vts.Bottom - 1), -(start + 1),
Orientation.Horizontal, lineStyle, attribute);
}
switch (endPos.side) {
case Side.Left:
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -(vts.Bottom - end),
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Top:
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Y), end + 1,
Orientation.Horizontal, lineStyle, attribute);
break;
case Side.Right:
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.Width - 1, vts.Y), end,
Orientation.Vertical, lineStyle, attribute);
break;
case Side.Bottom:
lc.AddLine (new Point (vts.X, vts.Bottom - 1), -vts.Height,
Orientation.Vertical, lineStyle, attribute);
lc.AddLine (new Point (vts.X, vts.Y), vts.Width,
Orientation.Horizontal, lineStyle, attribute);
lc.AddLine (new Point (vts.Width - 1, vts.Y), vts.Height,
Orientation.Vertical, lineStyle, attribute);
if (vts.Width - end > 0 && startPos.side == endPos.side) {
lc.AddLine (new Point (vts.Width - 1, vts.Bottom - 1), -(vts.Width - end),
Orientation.Horizontal, lineStyle, attribute);
}
break;
}
break;
}
if (mergeWithLineCanvas) {
LineCanvas.Merge (lc);
} else {
OnRenderLineCanvas ();
}
}
}
}

View File

@@ -2,6 +2,7 @@
using System;
using Xunit;
using Xunit.Abstractions;
using static Terminal.Gui.View;
namespace Terminal.Gui.ViewsTests {
public class DrawTests {
@@ -336,11 +337,11 @@ t ", output);
}
[Fact, AutoInitShutdown]
public void DrawFrame_Test ()
public void DrawFrame_Merge ()
{
var label = new View () { X = Pos.Center (), Y = Pos.Center (), Text = "test", AutoSize = true };
var view = new View () { Width = 10, Height = 5 };
view.DrawContentComplete += (s, e) => view.DrawFrame (view.Bounds, LineStyle.Single);
view.DrawContent += (s, e) => view.DrawFrame (view.Bounds, LineStyle.Single);
view.Add (label);
Application.Top.Add (view);
Application.Begin (Application.Top);
@@ -352,5 +353,316 @@ t ", output);
│ │
└────────┘", output);
}
[Fact, AutoInitShutdown]
public void DrawFrame_Without_Merge ()
{
var label = new View () { X = Pos.Center (), Y = Pos.Center (), Text = "test", AutoSize = true };
var view = new View () { Width = 10, Height = 5 };
view.DrawContentComplete += (s, e) => {
view.DrawFrame (view.Bounds, LineStyle.Single, null, false);
view.OnRenderLineCanvas ();
};
view.Add (label);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (@"
┌────────┐
│ │
│ test │
│ │
└────────┘", output);
}
[Theory, AutoInitShutdown]
[InlineData (1, Side.Left, 5, Side.Left, @"
┌────────┐
│ │
test │
─────────┘")]
[InlineData (1, Side.Left, 4, Side.Left, @"
┌────────┐
│ │
test │
└────────┘")]
[InlineData (0, Side.Left, 3, Side.Left, @"
┌────────┐
test │
│ │
└────────┘")]
[InlineData (5, Side.Top, -1, Side.Top, @"
│ ────┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (5, Side.Top, 0, Side.Top, @"
┌ ────┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (6, Side.Top, 1, Side.Top, @"
┌─ ───┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (7, Side.Top, 2, Side.Top, @"
┌── ──┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (8, Side.Top, 3, Side.Top, @"
┌─── ─┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (9, Side.Top, 4, Side.Top, @"
┌──── ┐
│ │
│ test │
│ │
└────────┘")]
[InlineData (10, Side.Top, 5, Side.Top, @"
┌───── │
│ │
│ test │
│ │
└────────┘")]
[InlineData (3, Side.Right, -1, Side.Right, @"
┌─────────
│ test
│ │
└────────┘")]
[InlineData (3, Side.Right, 0, Side.Right, @"
┌────────┐
│ test
│ │
└────────┘")]
[InlineData (4, Side.Right, 1, Side.Right, @"
┌────────┐
│ │
│ test
└────────┘")]
[InlineData (4, Side.Bottom, 10, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└──── │")]
[InlineData (4, Side.Bottom, 9, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└──── ┘")]
[InlineData (3, Side.Bottom, 8, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└─── ─┘")]
[InlineData (2, Side.Bottom, 7, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└── ──┘")]
[InlineData (1, Side.Bottom, 6, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└─ ───┘")]
[InlineData (0, Side.Bottom, 5, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
└ ────┘")]
[InlineData (-1, Side.Bottom, 5, Side.Bottom, @"
┌────────┐
│ │
│ test │
│ │
│ ────┘")]
public void DrawIncompleteFrame_All_Sides (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
private static View GetViewsForDrawFrameTests (int start, Side startSide, int end, Side endSide)
{
var label = new View () { X = Pos.Center (), Y = Pos.Center (), Text = "test", AutoSize = true };
var view = new View () { Width = 10, Height = 5 };
view.DrawContent += (s, e) =>
view.DrawIncompleteFrame (new (start, startSide), new (end, endSide), view.Bounds, LineStyle.Single);
view.Add (label);
return view;
}
[Theory, AutoInitShutdown]
[InlineData (4, Side.Left, 4, Side.Right, @"
┌────────┐
│ │
│ test │
│ │
│ │")]
[InlineData (0, Side.Top, 0, Side.Bottom, @"
─────────┐
test │
─────────┘")]
[InlineData (0, Side.Right, 0, Side.Left, @"
│ │
│ │
│ test │
│ │
└────────┘")]
[InlineData (9, Side.Bottom, 9, Side.Top, @"
┌─────────
│ test
└─────────")]
public void DrawIncompleteFrame_Three_Full_Sides (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
[Theory, AutoInitShutdown]
[InlineData (4, Side.Left, 10, Side.Top, @"
┌─────────
│ test
│ ")]
[InlineData (0, Side.Top, 5, Side.Right, @"
─────────┐
test │
│")]
[InlineData (0, Side.Right, 0, Side.Bottom, @"
test │
─────────┘")]
[InlineData (9, Side.Bottom, 0, Side.Left, @"
│ test
└─────────")]
public void DrawIncompleteFrame_Two_Full_Sides (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
[Theory, AutoInitShutdown]
[InlineData (4, Side.Left, 0, Side.Left, @"
│ test
│ ")]
[InlineData (0, Side.Top, 9, Side.Top, @"
──────────
test ")]
[InlineData (0, Side.Right, 4, Side.Right, @"
test │
│")]
[InlineData (9, Side.Bottom, 0, Side.Bottom, @"
test
──────────")]
public void DrawIncompleteFrame_One_Full_Sides (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
[Theory, AutoInitShutdown]
[InlineData (0, Side.Bottom, 0, Side.Top, @"
│ test
└ ")]
[InlineData (0, Side.Left, 0, Side.Right, @"
┌────────┐
test ")]
[InlineData (9, Side.Top, 9, Side.Bottom, @"
test │
┘")]
[InlineData (4, Side.Right, 4, Side.Left, @"
test
└────────┘")]
public void DrawIncompleteFrame_One_Full_Sides_With_Corner (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
[Theory, AutoInitShutdown]
[InlineData (2, Side.Left, 2, Side.Left, @"
│ test")]
[InlineData (3, Side.Top, 6, Side.Top, @"
────
test")]
[InlineData (2, Side.Right, 2, Side.Right, @"
test │")]
[InlineData (6, Side.Bottom, 3, Side.Bottom, @"
test
────")]
public void DrawIncompleteFrame_One_Part_Sides (int start, Side startSide, int end, Side endSide, string expected)
{
View view = GetViewsForDrawFrameTests (start, startSide, end, endSide);
Application.Top.Add (view);
Application.Begin (Application.Top);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
}
}
}