diff --git a/Examples/UICatalog/Scenarios/RegionScenario.cs b/Examples/UICatalog/Scenarios/RegionScenario.cs index 1fe69fd7f9..d5b5c16f0a 100644 --- a/Examples/UICatalog/Scenarios/RegionScenario.cs +++ b/Examples/UICatalog/Scenarios/RegionScenario.cs @@ -107,7 +107,45 @@ public override void Main () break; case RegionDrawStyles.OuterBoundary: - _region.DrawOuterBoundary (sendingView.LineCanvas, LineStyle.Single, tools.CurrentAttribute); + // Demo simplification: iterate the region's rectangles and add the four edges of each + // to the LineCanvas. Shared edges between adjacent rectangles will overlap; LineCanvas's + // line-style joining handles the visual result, matching how Border/Adornment draw. + foreach (Rectangle rect in _region.GetRectangles ()) + { + if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0) + { + continue; + } + + sendingView.LineCanvas.AddLine ( + new (rect.Left, rect.Top), + rect.Width, + Orientation.Horizontal, + LineStyle.Single, + tools.CurrentAttribute); + + sendingView.LineCanvas.AddLine ( + new (rect.Left, rect.Bottom - 1), + rect.Width, + Orientation.Horizontal, + LineStyle.Single, + tools.CurrentAttribute); + + sendingView.LineCanvas.AddLine ( + new (rect.Left, rect.Top), + rect.Height, + Orientation.Vertical, + LineStyle.Single, + tools.CurrentAttribute); + + sendingView.LineCanvas.AddLine ( + new (rect.Right - 1, rect.Top), + rect.Height, + Orientation.Vertical, + LineStyle.Single, + tools.CurrentAttribute); + } + _region.FillRectangles (sendingView.App?.Driver, tools.CurrentAttribute!.Value, (Rune)' '); break; diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs index 21718b0503..537cc5a127 100644 --- a/Terminal.Gui/Drawing/Region.cs +++ b/Terminal.Gui/Drawing/Region.cs @@ -982,203 +982,4 @@ public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attri } } } - - // BUGBUG: DrawOuterBoundary does not work right. it draws all regions +1 too tall/wide. It should draw single width/height regions as just a line. - // - // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4). - // This is how they should draw: - // - // |123456789|123456789|123456789 - // 1 │ ┌┐ ┌─┐ - // 2 │ ││ │ │ - // 3 │ ││ │ │ - // 4 │ └┘ └─┘ - // - // But this is what it draws: - // |123456789|123456789|123456789 - // 1┌┐ ┌─┐ ┌──┐ - // 2││ │ │ │ │ - // 3││ │ │ │ │ - // 4││ │ │ │ │ - // 5└┘ └─┘ └──┘ - // - // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3). - // This is fill - correct: - // |123456789 - // 1░░░ - // 2░░░ - // 3░░░░░ - // 4 ░░░ - // 5 ░░░ - // 6 - // - // This is what DrawOuterBoundary should draw - // |123456789|123456789 - // 1┌─┐ - // 2│ │ - // 3└─┼─┐ - // 4 │ │ - // 5 └─┘ - // 6 - // - // This is what DrawOuterBoundary actually draws - // |123456789|123456789 - // 1┌──┐ - // 2│ │ - // 3│ └─┐ - // 4└─┐ │ - // 5 │ │ - // 6 └──┘ - - /// - /// Draws the outer perimeter of the region to using and - /// . - /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing - /// boundaries and excluding internal lines. - /// - /// The LineCanvas to draw on. - /// The line style to use for drawing. - /// The attribute (color/style) to use for the lines. If null. - public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null) - { - if (_rectangles.Count == 0) - { - return; - } - - // Get the bounds of the region - Rectangle bounds = GetBounds (); - - // Add protection against extremely large allocations - if (bounds.Width > 1000 || bounds.Height > 1000) - { - // Fall back to drawing each rectangle's boundary - DrawBoundaries (lineCanvas, style, attribute); - - return; - } - - // Create a grid to track which cells are inside the region - var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1]; - - // Fill the grid based on rectangles - foreach (Rectangle rect in _rectangles) - { - if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0) - { - continue; - } - - for (int x = rect.Left; x < rect.Right; x++) - { - for (int y = rect.Top; y < rect.Bottom; y++) - { - // Adjust coordinates to grid space - int gridX = x - bounds.Left; - int gridY = y - bounds.Top; - - if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height) - { - insideRegion [gridX, gridY] = true; - } - } - } - } - - // Find horizontal boundary lines - for (var y = 0; y <= bounds.Height; y++) - { - int startX = -1; - - for (var x = 0; x <= bounds.Width; x++) - { - bool above = y > 0 && insideRegion [x, y - 1]; - bool below = y < bounds.Height && insideRegion [x, y]; - - // A boundary exists where one side is inside and the other is outside - bool isBoundary = above != below; - - if (isBoundary) - { - // Start a new segment or continue the current one - if (startX == -1) - { - startX = x; - } - } - else - { - // End the current segment if one exists - if (startX == -1) - { - continue; - } - int length = x - startX + 1; // Add 1 to make sure lines connect - - lineCanvas.AddLine (new Point (startX + bounds.Left, y + bounds.Top), length, Orientation.Horizontal, style, attribute); - startX = -1; - } - } - - // End any segment that reaches the right edge - if (startX == -1) - { - continue; - } - - { - int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect - - lineCanvas.AddLine (new Point (startX + bounds.Left, y + bounds.Top), length, Orientation.Horizontal, style, attribute); - } - } - - // Find vertical boundary lines - for (var x = 0; x <= bounds.Width; x++) - { - int startY = -1; - - for (var y = 0; y <= bounds.Height; y++) - { - bool left = x > 0 && insideRegion [x - 1, y]; - bool right = x < bounds.Width && insideRegion [x, y]; - - // A boundary exists where one side is inside and the other is outside - bool isBoundary = left != right; - - if (isBoundary) - { - // Start a new segment or continue the current one - if (startY == -1) - { - startY = y; - } - } - else - { - // End the current segment if one exists - if (startY == -1) - { - continue; - } - int length = y - startY + 1; // Add 1 to make sure lines connect - - lineCanvas.AddLine (new Point (x + bounds.Left, startY + bounds.Top), length, Orientation.Vertical, style, attribute); - startY = -1; - } - } - - // End any segment that reaches the bottom edge - if (startY == -1) - { - continue; - } - - { - int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect - - lineCanvas.AddLine (new Point (x + bounds.Left, startY + bounds.Top), length, Orientation.Vertical, style, attribute); - } - } - } } diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs deleted file mode 100644 index 4b45b514b2..0000000000 --- a/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs +++ /dev/null @@ -1,529 +0,0 @@ -using System.Collections.Concurrent; -namespace DrawingTests.RegionTests; - -/// -/// Tests for . -/// -public class DrawOuterBoundaryTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - [Fact] - public void DrawOuterBoundary_AfterIntersect_DrawsIntersectedBoundary () - { - // Arrange - var region = new Region (new (0, 0, 10, 10)); - region.Intersect (new Rectangle (5, 5, 10, 10)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_AfterMinimalUnion_DrawsMinimalBoundary () - { - // Arrange - var region = new Region (new (0, 0, 3, 3)); - region.MinimalUnion (new Rectangle (3, 0, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_ComplexShape_HandlesMultipleRegions () - { - // Arrange - Create a complex shape with multiple rectangles - var region = new Region (new (0, 0, 3, 3)); - region.Union (new Rectangle (3, 3, 3, 3)); - region.Union (new Rectangle (6, 0, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_DiagonallyConnectedRectangles_DrawsOuterBoundary () - { - // Arrange - Test the specific case from BUGBUG comment: (0,0,3,3) and (3,3,3,3) - var region = new Region (new (0, 0, 3, 3)); - region.Union (new Rectangle (3, 3, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Note: According to BUGBUG comment, this should draw specific shape - // with connecting corner but currently draws incorrectly - } - - [Fact] - public void DrawOuterBoundary_EmptyRegion_DoesNotThrow () - { - // Arrange - var region = new Region (); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - Assert.Empty (canvas.GetCellMap ()); - } - - [Fact] - public void DrawOuterBoundary_GridPattern_DrawsOuterBoundary () - { - // Arrange - Create a checkerboard pattern - var region = new Region (new (0, 0, 2, 2)); - region.Union (new Rectangle (4, 0, 2, 2)); - region.Union (new Rectangle (0, 4, 2, 2)); - region.Union (new Rectangle (4, 4, 2, 2)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_HollowRectangle_DrawsOuterAndInnerBoundaries () - { - // Arrange - Create a hollow rectangle (outer rect with inner rect removed) - var region = new Region (new (0, 0, 10, 10)); - region.Exclude (new Rectangle (2, 2, 6, 6)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_HorizontalLineRectangle_DrawsHorizontalLine () - { - // Arrange - A horizontal line (width>1, height=1) - var region = new Region (new (0, 0, 4, 1)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_LShapedRegion_DrawsLShapeBoundary () - { - // Arrange - Create an L-shape - var region = new Region (new (0, 0, 3, 3)); - region.Union (new Rectangle (0, 3, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_MultipleCallsOnSameCanvas_AccumulatesLines () - { - // Arrange - var region1 = new Region (new (0, 0, 3, 3)); - var region2 = new Region (new (5, 5, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region1.DrawOuterBoundary (canvas, LineStyle.Single); - int cellCountAfterFirst = canvas.GetCellMap ().Count; - - region2.DrawOuterBoundary (canvas, LineStyle.Single); - int cellCountAfterSecond = canvas.GetCellMap ().Count; - - // Assert - Assert.True (cellCountAfterSecond >= cellCountAfterFirst); - } - - [Fact] - public void DrawOuterBoundary_MultipleRegionsWithGaps_DrawsSeparateBoundaries () - { - // Arrange - var region = new Region (); - region.Union (new Rectangle (0, 0, 2, 2)); - region.Union (new Rectangle (4, 0, 2, 2)); - region.Union (new Rectangle (8, 0, 2, 2)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Should have three separate boundary regions - } - - [Fact] - public void DrawOuterBoundary_OverlappingRectangles_DrawsOuterBoundaryOnly () - { - // Arrange - Two overlapping rectangles - var region = new Region (new (0, 0, 5, 5)); - region.Union (new Rectangle (3, 3, 5, 5)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Should only draw outer perimeter, not the overlapping internal area - } - - [Fact] - public void DrawOuterBoundary_RectangleAtNegativeCoordinates_DrawsBoundary () - { - // Arrange - var region = new Region (new (-5, -5, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_SinglePixelRectangle_DrawsSinglePoint () - { - // Arrange - A 1x1 rectangle - var region = new Region (new (5, 5, 1, 1)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_SingleRectangle_DrawsBoundary () - { - // Arrange - var region = new Region (new (0, 0, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_SingleWidthRegion_DrawsCorrectly () - { - // Arrange - Test the specific case mentioned in BUGBUG comment - var region = new Region (new (0, 0, 1, 4)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Note: According to BUGBUG, this should draw a single vertical line - // but currently draws too wide - } - - [Fact] - public void DrawOuterBoundary_ThreadSafety_MultipleThreadsDrawing () - { - // Arrange - var region = new Region (new (0, 0, 10, 10)); - ConcurrentBag exceptions = new (); - - // Act - Parallel.For ( - 0, - 10, - i => - { - try - { - var canvas = new LineCanvas (); - region.DrawOuterBoundary (canvas, LineStyle.Single); - } - catch (Exception ex) - { - exceptions.Add (ex); - } - }); - - // Assert - Assert.Empty (exceptions); - } - - [Fact] - public void DrawOuterBoundary_ThreeWidthRegion_DrawsCorrectly () - { - // Arrange - Test the specific case mentioned in BUGBUG comment - var region = new Region (new (20, 0, 3, 4)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_TShapedRegion_DrawsCorrectBoundary () - { - // Arrange - Create a T-shape - var region = new Region (new (0, 0, 9, 3)); // Horizontal bar - region.Union (new Rectangle (3, 3, 3, 6)); // Vertical bar - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_TwoAdjacentRectangles_DrawsOuterPerimeter () - { - // Arrange - Two rectangles side by side - var region = new Region (new (0, 0, 3, 3)); - region.Union (new Rectangle (3, 0, 3, 3)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Should draw outer boundary, not internal dividing line - // The combined region should be treated as one shape - } - - [Fact] - public void DrawOuterBoundary_TwoSeparateRectangles_DrawsTwoBoundaries () - { - // Arrange - Two non-adjacent rectangles - var region = new Region (new (0, 0, 2, 2)); - region.Union (new Rectangle (5, 5, 2, 2)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - - // Should have boundaries for both rectangles - Assert.True (cells.Count > 0); - } - - [Fact] - public void DrawOuterBoundary_TwoWidthRegion_DrawsCorrectly () - { - // Arrange - Test the specific case mentioned in BUGBUG comment - var region = new Region (new (10, 0, 2, 4)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Theory] - [InlineData (0, 0)] - [InlineData (10, 10)] - [InlineData (-5, -5)] - [InlineData (100, 100)] - public void DrawOuterBoundary_VariousPositions_DrawsBoundary (int x, int y) - { - // Arrange - var region = new Region (new (x, y, 5, 5)); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Theory] - [InlineData (1, 1)] - [InlineData (1, 5)] - [InlineData (5, 1)] - [InlineData (2, 2)] - [InlineData (10, 10)] - [InlineData (100, 100)] - public void DrawOuterBoundary_VariousSizes_DrawsBoundary (int width, int height) - { - // Arrange - var region = new Region (new (0, 0, width, height)); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_VerticalLineRectangle_DrawsVerticalLine () - { - // Arrange - A vertical line (width=1, height>1) - var region = new Region (new (0, 0, 1, 4)); - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_VeryLargeRegion_FallsBackToDrawBoundaries () - { - // Arrange - Create a region larger than the 1000x1000 threshold - var region = new Region (new (0, 0, 1100, 1100)); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - - // Should fall back to DrawBoundaries for very large regions - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_WithCustomAttribute_AppliesAttribute () - { - // Arrange - var region = new Region (new (0, 0, 3, 3)); - var canvas = new LineCanvas (); - var attribute = new Attribute (Color.Red, Color.Blue); - - // Act - region.DrawOuterBoundary (canvas, LineStyle.Single, attribute); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - - [Fact] - public void DrawOuterBoundary_WithDifferentLineStyles_DrawsWithCorrectStyle () - { - // Arrange - var region = new Region (new (0, 0, 3, 3)); - - // Test each line style - foreach (LineStyle style in Enum.GetValues ()) - { - var canvas = new LineCanvas (); - - // Act - region.DrawOuterBoundary (canvas, style); - - // Assert - Dictionary cells = canvas.GetCellMap (); - Assert.NotEmpty (cells); - } - } - - [Fact] - public void DrawOuterBoundary_ZeroHeightRectangle_HandlesGracefully () - { - // Arrange - Rectangle with zero height - var region = new Region (new (5, 5, 5, 0)); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - } - - [Fact] - public void DrawOuterBoundary_ZeroWidthRectangle_HandlesGracefully () - { - // Arrange - Rectangle with zero width - var region = new Region (new (5, 5, 0, 5)); - var canvas = new LineCanvas (); - - // Act - Exception? exception = Record.Exception (() => region.DrawOuterBoundary (canvas, LineStyle.Single)); - - // Assert - Assert.Null (exception); - } -}