diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index 7eb4e7f600..033731307f 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -61,27 +61,25 @@ public bool NeedsDisplay public virtual bool SuperViewRendersLineCanvas { get; set; } = false; /// Draws the specified character in the specified viewport-relative column and row of the View. + /// + /// If the provided coordinates are outside the visible content area, this method does nothing. + /// /// /// The top-left corner of the visible content area is ViewPort.Location. /// /// Column (viewport-relative). /// Row (viewport-relative). - /// Ch. - public void AddRune (int col, int row, Rune ch) + /// The Rune. + public void AddRune (int col, int row, Rune rune) { - if (row < 0 || col < 0) - { - return; - } - - // BUGBUG: This should be Viewport.Size - if (row > _frame.Height - 1 || col > _frame.Width - 1) + if (row < 0 || col < 0 || row >= Viewport.Height || col >= Viewport.Width) { + // TODO: Change return type to bool so callers can determine success? return; } Move (col, row); - Driver.AddRune (ch); + Driver.AddRune (rune); } /// Clears with the normal background. @@ -320,9 +318,14 @@ public virtual Attribute GetNormalColor () return Enabled ? cs.Normal : cs.Disabled; } - /// This moves the cursor to the specified view-relative column and row in the view. + /// Moves the drawing cursor to the specified view-relative column and row in the view. /// + /// + /// If the provided coordinates are outside the visible content area, this method does nothing. + /// + /// /// The top-left corner of the visible content area is ViewPort.Location. + /// /// /// Column (viewport-relative). /// Row (viewport-relative). @@ -333,9 +336,13 @@ public void Move (int col, int row) return; } - Rectangle screen = ViewportToScreen (new (col, row, 0, 0)); + if (col < 0 || row < 0 || col >= Viewport.Size.Width || row >= Viewport.Size.Height) + { + // TODO: Change return type to bool so callers can determine success? + return; + } - // TODO: Clamp this to stay within the View's Viewport + Rectangle screen = ViewportToScreen (new (col, row, 0, 0)); Driver?.Move (screen.X, screen.Y); } diff --git a/UnitTests/View/DrawTests.cs b/UnitTests/View/DrawTests.cs index 38cffae9ac..0dde48c882 100644 --- a/UnitTests/View/DrawTests.cs +++ b/UnitTests/View/DrawTests.cs @@ -4,18 +4,70 @@ namespace Terminal.Gui.ViewTests; -[Trait("Category","Output")] -public class DrawTests +[Trait ("Category", "Output")] +public class DrawTests (ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - public DrawTests (ITestOutputHelper output) { _output = output; } + [Fact] + [SetupFakeDriver] + public void Move_Is_Constrained_To_Viewport () + { + var view = new View + { + X = 1, + Y = 1, + Width = 3, Height = 3 + }; + view.Margin.Thickness = new Thickness (1); + + // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) + + view.Move (0, 0); + Assert.Equal(new Point(2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + + view.Move (-1, -1); + Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + + view.Move (1, 1); + Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row)); + } + + [Fact] + [SetupFakeDriver] + public void AddRune_Is_Constrained_To_Viewport () + { + var view = new View + { + X = 1, + Y = 1, + Width = 3, Height = 3 + }; + view.Margin.Thickness = new Thickness (1); + View.Diagnostics = ViewDiagnosticFlags.Padding; + view.BeginInit(); + view.EndInit(); + view.Draw(); + + // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen) + Assert.Equal ((Rune)' ', Application.Driver.Contents [2, 2].Rune); + + view.AddRune(0, 0, Rune.ReplacementChar); + Assert.Equal (Rune.ReplacementChar, Application.Driver.Contents [2, 2].Rune); + + view.AddRune (-1, -1, Rune.ReplacementChar); + Assert.Equal ((Rune)'M', Application.Driver.Contents [1, 1].Rune); + + view.AddRune (1, 1, Rune.ReplacementChar); + Assert.Equal ((Rune)'M', Application.Driver.Contents [3, 3].Rune); + + View.Diagnostics = ViewDiagnosticFlags.Off; + } [Theory] [InlineData (0, 0, 1, 1)] [InlineData (0, 0, 2, 2)] [InlineData (-1, -1, 2, 2)] [SetupFakeDriver] - public void Clear_Viewport_Clears_Only_Bounds (int x, int y, int width, int height) + public void Clear_Clears_Only_Viewport (int x, int y, int width, int height) { var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () }; @@ -32,28 +84,80 @@ public void Clear_Viewport_Clears_Only_Bounds (int x, int y, int width, int heig superView.LayoutSubviews (); superView.Draw (); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │X│ + └─┘", + output); + Rectangle toClear = new (x, y, width, height); + view.Clear (toClear); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │ │ + └─┘", + output); + // Now try to clear beyond Viewport (invalid) + superView.SetNeedsDisplay (); + superView.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" ┌─┐ │X│ └─┘", - _output); + output); + toClear = new (-width, -height, width, height); + view.Clear (toClear); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │X│ + └─┘", + output); - Rectangle boundsToClear = new (x, y, width, height); - view.Clear (boundsToClear); + // Now try to clear beyond Viewport (valid) + superView.SetNeedsDisplay (); + superView.Draw (); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │X│ + └─┘", + output); + toClear = new (-1, -1, width + 1, height + 1); + view.Clear (toClear); TestHelpers.AssertDriverContentsWithFrameAre ( @" ┌─┐ │ │ └─┘", - _output); + output); + // Now clear too much size + superView.SetNeedsDisplay (); + superView.Draw (); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │X│ + └─┘", + output); + toClear = new (0, 0, width * 2, height * 2); + view.Clear (toClear); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌─┐ + │ │ + └─┘", + output); } + [Fact] [AutoInitShutdown] - [Trait("Category","Unicode")] + [Trait ("Category", "Unicode")] public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two () { const string us = "\U0000f900"; @@ -83,9 +187,9 @@ public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two () │豈 │ └────────┘ """; - TestHelpers.AssertDriverContentsWithFrameAre (expectedOutput, _output); + TestHelpers.AssertDriverContentsWithFrameAre (expectedOutput, output); - TestHelpers.AssertDriverContentsAre (expectedOutput, _output); + TestHelpers.AssertDriverContentsAre (expectedOutput, output); Attribute [] expectedColors = { @@ -115,7 +219,7 @@ Colors.ColorSchemes ["Base"].HotNormal // TODO: Refactor this test to not depend on TextView etc... Make it as primitive as possible [Fact] [AutoInitShutdown] - [Trait("Category","Unicode")] + [Trait ("Category", "Unicode")] public void Clipping_AddRune_Left_Or_Right_Replace_Previous_Or_Next_Wide_Rune_With_Space () { var tv = new TextView @@ -161,13 +265,13 @@ public void Clipping_AddRune_Left_Or_Right_Replace_Previous_Or_Next_Wide_Rune_Wi └────────────────────────────┘ """; - Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expectedOutput, _output); + Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expectedOutput, output); Assert.Equal (new Rectangle (0, 0, 30, 10), pos); } [Fact] [AutoInitShutdown] - [Trait("Category","Output")] + [Trait ("Category", "Output")] public void Colors_On_TextAlignment_Right_And_Bottom () { var viewRight = new View @@ -206,7 +310,7 @@ public void Colors_On_TextAlignment_Right_And_Bottom () s t """, - _output + output ); TestHelpers.AssertDriverAttributesAre ( @@ -227,14 +331,14 @@ Colors.ColorSchemes ["Base"].Normal [Fact] [SetupFakeDriver] - public void Draw_Minimum_Full_Border_With_Empty_Bounds () + public void Draw_Minimum_Full_Border_With_Empty_Viewport () { var view = new View { Width = 2, Height = 2, BorderStyle = LineStyle.Single }; view.BeginInit (); view.EndInit (); view.SetRelativeLayout (Application.Driver.Viewport); - Assert.Equal (new (0,0,2,2), view.Frame); + Assert.Equal (new (0, 0, 2, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); view.Draw (); @@ -245,7 +349,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Bounds () ┌┐ └┘ """, - _output + output ); } @@ -259,12 +363,12 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Bottom () view.EndInit (); view.SetRelativeLayout (Application.Driver.Viewport); - Assert.Equal (new (0,0,2,1), view.Frame); + Assert.Equal (new (0, 0, 2, 1), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); view.Draw (); - TestHelpers.AssertDriverContentsWithFrameAre (string.Empty, _output); + TestHelpers.AssertDriverContentsWithFrameAre (string.Empty, output); } [Fact] @@ -277,7 +381,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Left () view.EndInit (); view.SetRelativeLayout (Application.Driver.Viewport); - Assert.Equal (new (0,0,1,2), view.Frame); + Assert.Equal (new (0, 0, 1, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); view.Draw (); @@ -288,7 +392,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Left () │ │ """, - _output + output ); } @@ -302,7 +406,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Right () view.EndInit (); view.SetRelativeLayout (Application.Driver.Viewport); - Assert.Equal (new (0,0,1,2), view.Frame); + Assert.Equal (new (0, 0, 1, 2), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); view.Draw (); @@ -313,7 +417,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Right () │ │ """, - _output + output ); } @@ -328,7 +432,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Top () view.EndInit (); view.SetRelativeLayout (Application.Driver.Viewport); - Assert.Equal (new (0,0,2,1), view.Frame); + Assert.Equal (new (0, 0, 2, 1), view.Frame); Assert.Equal (Rectangle.Empty, view.Viewport); view.Draw (); @@ -339,7 +443,7 @@ public void Draw_Minimum_Full_Border_With_Empty_Viewport_Without_Top () ┌┐ """, - _output + output ); } @@ -416,7 +520,7 @@ public void Draw_Negative_Viewport_Horizontal_With_New_Lines () 3V 4i """, - _output + output ); content.X = -1; @@ -431,12 +535,12 @@ public void Draw_Negative_Viewport_Horizontal_With_New_Lines () V i """, - _output + output ); content.X = -2; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre (@"", _output); + TestHelpers.AssertDriverContentsWithFrameAre (@"", output); content.X = 0; content.Y = -1; @@ -451,7 +555,7 @@ public void Draw_Negative_Viewport_Horizontal_With_New_Lines () 4i 5e """, - _output + output ); content.Y = -6; @@ -466,7 +570,7 @@ public void Draw_Negative_Viewport_Horizontal_With_New_Lines () 9 0 """, - _output + output ); content.Y = -19; @@ -477,17 +581,17 @@ public void Draw_Negative_Viewport_Horizontal_With_New_Lines () 9 """, - _output + output ); content.Y = -20; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); content.X = -2; content.Y = 0; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); } [Fact] @@ -533,7 +637,7 @@ public void Draw_Negative_Viewport_Horizontal_Without_New_Lines () 01234 subVi """, - _output + output ); content.X = -1; @@ -545,7 +649,7 @@ public void Draw_Negative_Viewport_Horizontal_Without_New_Lines () 12345 ubVie """, - _output + output ); content.Y = -1; @@ -556,17 +660,17 @@ public void Draw_Negative_Viewport_Horizontal_Without_New_Lines () ubVie """, - _output + output ); content.Y = -2; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); content.X = -20; content.Y = 0; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); } [Fact] @@ -618,7 +722,7 @@ public void Draw_Negative_Viewport_Vertical () 3V 4i """, - _output + output ); content.X = -1; @@ -633,12 +737,12 @@ public void Draw_Negative_Viewport_Vertical () V i """, - _output + output ); content.X = -2; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre (@"", _output); + TestHelpers.AssertDriverContentsWithFrameAre (@"", output); content.X = 0; content.Y = -1; @@ -653,7 +757,7 @@ public void Draw_Negative_Viewport_Vertical () 4i 5e """, - _output + output ); content.Y = -6; @@ -668,7 +772,7 @@ public void Draw_Negative_Viewport_Vertical () 9 0 """, - _output + output ); content.Y = -19; @@ -679,17 +783,17 @@ public void Draw_Negative_Viewport_Vertical () 9 """, - _output + output ); content.Y = -20; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); content.X = -2; content.Y = 0; Application.Refresh (); - TestHelpers.AssertDriverContentsWithFrameAre ("", _output); + TestHelpers.AssertDriverContentsWithFrameAre ("", output); } [Theory] @@ -701,7 +805,7 @@ public void DrawHotString_NonBmp (string expected) var view = new View { Width = 10, Height = 1 }; view.DrawHotString (expected, Attribute.Default, Attribute.Default); - TestHelpers.AssertDriverContentsWithFrameAre (expected, _output); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); } // TODO: The tests below that use Label should use View instead. @@ -736,9 +840,9 @@ public void Non_Bmp_ConsoleWidth_ColumnWidth_Equal_Two () │𝔹 │ └────────┘ """; - TestHelpers.AssertDriverContentsWithFrameAre (expected, _output); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - TestHelpers.AssertDriverContentsAre (expected, _output); + TestHelpers.AssertDriverContentsAre (expected, output); Attribute [] expectedColors = {