diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index a36093906b..d49d4ae0b7 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -4,6 +4,28 @@ namespace Terminal.Gui { public partial class View { + /// + /// Specifies the side to start when draw frame with + /// method. + /// + public enum Side { + /// + /// Start on left. + /// + Left, + /// + /// Start on top. + /// + Top, + /// + /// Start on right. + /// + Right, + /// + /// Start on bottom. + /// + Bottom + }; ColorScheme _colorScheme; @@ -184,7 +206,7 @@ public void SetSubViewNeedsDisplay () /// This clears the Bounds used by this view. /// /// - 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 @@ public virtual void OnDrawContentComplete (Rect contentArea) } /// - /// 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 is true, otherwise will be rendered immediately. /// - /// The bounds view relative. + /// The view relative location and size of the frame. /// The line style. - /// The color to use. - public void DrawFrame (Rect bounds, LineStyle lineStyle, Attribute? attribute = null) + /// The colors to be used. + /// 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. + 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 (); + } + } + + /// + /// Draws an incomplete frame. The frame will be merged (auto-joined) with any other lines drawn by this View + /// if is true, otherwise will be rendered immediately. + /// The frame is always drawn clockwise. For and the end position must + /// be greater or equal to the start and for and the end position must + /// be less or equal to the start. + /// + /// The start and side position screen relative. + /// The end and side position screen relative. + /// The view relative location and size of the frame. + /// The line style. + /// The colors to be used. + /// 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. + 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 (); + } } } } \ No newline at end of file diff --git a/UnitTests/View/DrawTests.cs b/UnitTests/View/DrawTests.cs index 8027710b43..d5fa06c771 100644 --- a/UnitTests/View/DrawTests.cs +++ b/UnitTests/View/DrawTests.cs @@ -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 @@ public void Draw_Negative_Bounds_Vertical () } [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 @@ public void DrawFrame_Test () │ │ └────────┘", 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); + } } }