Fixes #4258 - Glyphs drawn at mid-point of wide glyphs don't get drawn with clipping (#4462)

* Enhanced `View.Drawing.cs` with improved comments, a new
`DoDrawComplete` method for clip region updates, and
clarified terminology. Added detailed remarks for the
`OnDrawComplete` method and `DrawComplete` event.

Refactored `ViewDrawingClippingTests` to simplify driver
setup, use target-typed `new`, and add a new test for wide
glyph clipping with bordered subviews. Improved handling of
edge cases like empty viewports and nested clips.

Added `WideGlyphs.DrawFlow.md` and
`ViewDrawingClippingTests.DrawFlow.md` to document the draw
flow, clipping behavior, and coordinate systems for both the
scenario and the test.

Commented out redundant `Driver.Clip` initialization in
`ApplicationImpl`. Added a `BUGBUG` comment in `Border` to
highlight missing redraw logic for `LineStyle` changes.

* Uncomment Driver.Clip initialization in Screen redraw

* Fixed it!

* Fixes #4258 - Correct wide glyph and border rendering

Refactored `OutputBufferImpl.AddStr` to improve handling of wide glyphs:
- Wide glyphs now modify only the first column they occupy, leaving the second column untouched.
- Removed redundant code that set replacement characters and marked cells as not dirty.
- Synchronized cursor updates (`Col` and `Row`) with the buffer lock to prevent race conditions.
- Modularized logic with helper methods for better readability and maintainability.

Updated `WideGlyphs.cs`:
- Removed dashed `BorderStyle` and added border thickness and subview for `arrangeableViewAtEven`.
- Removed unused `superView` initialization.

Enhanced tests:
- Added unit tests to verify correct rendering of borders and content at odd columns overlapping wide glyphs.
- Updated existing tests to reflect the new behavior of wide glyph handling.
- Introduced `DriverAssert.AssertDriverOutputIs` to validate raw ANSI output.

Improved documentation:
- Expanded problem description and root cause analysis in `WideGlyphBorderBugFix.md`.
- Detailed the fix and its impact, ensuring proper layering of content at any column position.

General cleanup:
- Removed unused imports and redundant code.
- Improved code readability and maintainability.

* Code cleanup

* Update Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/Drivers/OutputBufferImpl.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fixed test slowness problem

* Simplified

* Rmoved temp .md files

* Refactor I/O handling and improve testability

Refactored `InputProcessor` and `Output` access by replacing direct property usage with `GetInputProcessor()` and `GetOutput()` methods to enhance encapsulation. Introduced `GetLastOutput()` and `GetLastBuffer()` methods for better debugging and testability.

Centralized `StringBuilder` usage in `OutputBase` implementations to ensure consistency. Improved exception handling with clearer messages. Updated tests to align with the refactored structure and added a new test for wide glyph handling.

Enhanced ANSI sequence handling and simplified cursor visibility logic to prevent flickering. Standardized method naming for consistency. Cleaned up redundant code and improved documentation for better developer clarity.

* Refactored `NetOutput`, `FakeOutput`, `UnixOutput`, and `WindowsOutput` classes to support access to `Output` and added a `IDriver.GetOutput` to acess the `IOutput`. `IOutput` now has a `GetLastOutput` method.

Simplified `DriverAssert` logic and enhanced `DriverTests` with a new test for wide glyph clipping across drivers.

Performed general cleanup, including removal of unused code, improved formatting, and adoption of modern C# practices. Added `using System.Diagnostics` in `OutputBufferImpl` for debugging.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tig
2025-12-08 12:28:32 -07:00
committed by GitHub
parent 5da7e59aa2
commit f548059a27
20 changed files with 1046 additions and 324 deletions

View File

@@ -1,19 +1,18 @@
#nullable enable
using System.Text;
using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Drawing;
public class ViewDrawingClippingTests () : FakeDriverBase
public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBase
{
#region GetClip / SetClip Tests
[Fact]
public void GetClip_ReturnsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var region = new Region (new Rectangle (10, 10, 20, 20));
IDriver driver = CreateFakeDriver ();
var region = new Region (new (10, 10, 20, 20));
driver.Clip = region;
View view = new () { Driver = driver };
@@ -26,8 +25,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_NullRegion_DoesNothing ()
{
IDriver driver = CreateFakeDriver (80, 25);
var original = new Region (new Rectangle (5, 5, 10, 10));
IDriver driver = CreateFakeDriver ();
var original = new Region (new (5, 5, 10, 10));
driver.Clip = original;
View view = new () { Driver = driver };
@@ -40,8 +39,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_ValidRegion_SetsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var region = new Region (new Rectangle (10, 10, 30, 30));
IDriver driver = CreateFakeDriver ();
var region = new Region (new (10, 10, 30, 30));
View view = new () { Driver = driver };
view.SetClip (region);
@@ -56,8 +55,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClipToScreen_ReturnsPreviousClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var original = new Region (new Rectangle (5, 5, 10, 10));
IDriver driver = CreateFakeDriver ();
var original = new Region (new (5, 5, 10, 10));
driver.Clip = original;
View view = new () { Driver = driver };
@@ -70,7 +69,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClipToScreen_SetsClipToScreen ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
View view = new () { Driver = driver };
view.SetClipToScreen ();
@@ -87,15 +86,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
public void ExcludeFromClip_Rectangle_NullDriver_DoesNotThrow ()
{
View view = new () { Driver = null };
var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
Assert.Null (exception);
}
[Fact]
public void ExcludeFromClip_Rectangle_ExcludesArea ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (0, 0, 80, 25));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (0, 0, 80, 25));
View view = new () { Driver = driver };
var toExclude = new Rectangle (10, 10, 20, 20);
@@ -111,19 +110,18 @@ public class ViewDrawingClippingTests () : FakeDriverBase
{
View view = new () { Driver = null };
var exception = Record.Exception (() => view.ExcludeFromClip (new Region (new Rectangle (5, 5, 10, 10))));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Region (new (5, 5, 10, 10))));
Assert.Null (exception);
}
[Fact]
public void ExcludeFromClip_Region_ExcludesArea ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (0, 0, 80, 25));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (0, 0, 80, 25));
View view = new () { Driver = driver };
var toExclude = new Region (new Rectangle (10, 10, 20, 20));
var toExclude = new Region (new (10, 10, 20, 20));
view.ExcludeFromClip (toExclude);
// Verify the region was excluded
@@ -150,8 +148,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddFrameToClip_IntersectsWithFrame ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -171,7 +169,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.NotNull (driver.Clip);
// The clip should now be the intersection of the screen and the view's frame
Rectangle expectedBounds = new Rectangle (1, 1, 20, 20);
var expectedBounds = new Rectangle (1, 1, 20, 20);
Assert.Equal (expectedBounds, driver.Clip.GetBounds ());
}
@@ -194,8 +192,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_IntersectsWithViewport ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -222,8 +220,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_WithClipContentOnly_LimitsToVisibleContent ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -260,7 +258,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
public void ClipRegions_StackCorrectly_WithNestedViews ()
{
IDriver driver = CreateFakeDriver (100, 100);
driver.Clip = new Region (driver.Screen);
driver.Clip = new (driver.Screen);
var superView = new View
{
@@ -278,7 +276,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
X = 5,
Y = 5,
Width = 30,
Height = 30,
Height = 30
};
superView.Add (view);
superView.LayoutSubViews ();
@@ -296,14 +294,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Restore superView clip
view.SetClip (superViewClip);
// Assert.Equal (superViewBounds, driver.Clip.GetBounds ());
}
[Fact]
public void ClipRegions_RespectPreviousClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var initialClip = new Region (new Rectangle (20, 20, 40, 40));
IDriver driver = CreateFakeDriver ();
var initialClip = new Region (new (20, 20, 40, 40));
driver.Clip = initialClip;
var view = new View
@@ -322,9 +321,9 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// The new clip should be the intersection of the initial clip and the view's frame
Rectangle expected = Rectangle.Intersect (
initialClip.GetBounds (),
view.FrameToScreen ()
);
initialClip.GetBounds (),
view.FrameToScreen ()
);
Assert.Equal (expected, driver.Clip.GetBounds ());
@@ -340,8 +339,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddFrameToClip_EmptyFrame_WorksCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -364,18 +363,18 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_EmptyViewport_WorksCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
X = 1,
Y = 1,
Width = 1, // Minimal size to have adornments
Width = 1, // Minimal size to have adornments
Height = 1,
Driver = driver
};
view.Border!.Thickness = new Thickness (1);
view.Border!.Thickness = new (1);
view.BeginInit ();
view.EndInit ();
view.LayoutSubViews ();
@@ -391,12 +390,12 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void ClipRegions_OutOfBounds_HandledCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
X = 100, // Outside screen bounds
X = 100, // Outside screen bounds
Y = 100,
Width = 20,
Height = 20,
@@ -409,6 +408,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Region? previous = view.AddFrameToClip ();
Assert.NotNull (previous);
// The clip should be empty since the view is outside the screen
Assert.True (driver.Clip.IsEmpty () || !driver.Clip.Contains (100, 100));
}
@@ -420,8 +420,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Clip_Set_BeforeDraw_ClipsDrawing ()
{
IDriver driver = CreateFakeDriver (80, 25);
var clip = new Region (new Rectangle (10, 10, 10, 10));
IDriver driver = CreateFakeDriver ();
var clip = new Region (new (10, 10, 10, 10));
driver.Clip = clip;
var view = new View
@@ -445,8 +445,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_UpdatesDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -464,14 +464,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Clip should be updated to exclude the drawn view
Assert.NotNull (driver.Clip);
// Assert.False (driver.Clip.Contains (15, 15)); // Point inside the view should be excluded
}
[Fact]
public void Draw_WithSubViews_ClipsCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var superView = new View
{
@@ -491,13 +492,277 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Both superView and view should be excluded from clip
Assert.NotNull (driver.Clip);
// Assert.False (driver.Clip.Contains (15, 15)); // Point in superView should be excluded
}
/// <summary>
/// Tests that wide glyphs (🍎) are correctly clipped when overlapped by bordered subviews
/// at different column alignments (even vs odd). Demonstrates:
/// 1. Full clipping at even columns (X=0, X=2)
/// 2. Partial clipping at odd columns (X=1) resulting in half-glyphs (<28>)
/// 3. The recursive draw flow and clip exclusion mechanism
///
/// For detailed draw flow documentation, see ViewDrawingClippingTests.DrawFlow.md
/// </summary>
[Fact]
public void Draw_WithBorderSubView_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (30, 20);
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Auto () + 4,
Height = Dim.Auto () + 1,
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
var view = s as View;
for (var r = 0; r < view!.Viewport.Height; r++)
{
for (var c = 0; c < view.Viewport.Width; c += 2)
{
if (codepoint != default (Rune))
{
view.AddRune (c, r, codepoint);
}
}
}
e.DrawContext?.AddDrawnRectangle (view.Viewport);
e.Cancel = true;
};
var viewWithBorderAtX0 = new View
{
Text = "viewWithBorderAtX0",
BorderStyle = LineStyle.Dashed,
X = 0,
Y = 1,
Width = Dim.Auto (),
Height = 3
};
var viewWithBorderAtX1 = new View
{
Text = "viewWithBorderAtX1",
BorderStyle = LineStyle.Dashed,
X = 1,
Y = Pos.Bottom (viewWithBorderAtX0) + 1,
Width = Dim.Auto (),
Height = 3
};
var viewWithBorderAtX2 = new View
{
Text = "viewWithBorderAtX2",
BorderStyle = LineStyle.Dashed,
X = 2,
Y = Pos.Bottom (viewWithBorderAtX1) + 1,
Width = Dim.Auto (),
Height = 3
};
superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2);
app.Begin (superView);
// Begin calls LayoutAndDraw, so no need to call it again here
// app.LayoutAndDraw();
DriverAssert.AssertDriverContentsAre (
"""
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
viewWithBorderAtX0🍎🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
<EFBFBD> 🍎🍎
<EFBFBD>viewWithBorderAtX1 🍎🍎
<EFBFBD> 🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
🍎viewWithBorderAtX2🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output After Redraw:\n" + driver.GetOutput().GetLastOutput());
// BUGBUG: Border.set_LineStyle does not call SetNeedsDraw
viewWithBorderAtX1!.Border!.LineStyle = LineStyle.Single;
viewWithBorderAtX1.Border!.SetNeedsDraw ();
app.LayoutAndDraw ();
DriverAssert.AssertDriverContentsAre (
"""
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
viewWithBorderAtX0🍎🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
<EFBFBD> 🍎🍎
<EFBFBD>viewWithBorderAtX1 🍎🍎
<EFBFBD> 🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
🍎viewWithBorderAtX2🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
""",
output,
driver);
}
[Fact]
public void Draw_WithBorderSubView_At_Col1_In_WideGlyph_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (6, 3); // Minimal: 6 cols wide (3 for content + 2 for border + 1), 3 rows high (1 for content + 2 for border)
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill (),
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
View? view = s as View;
view?.AddStr (0, 0, "🍎🍎🍎🍎");
view?.AddStr (0, 1, "🍎🍎🍎🍎");
view?.AddStr (0, 2, "🍎🍎🍎🍎");
e.DrawContext?.AddDrawnRectangle (view!.Viewport);
e.Cancel = true;
};
// Minimal border at X=1 (odd column), Width=3, Height=3 (includes border)
var viewWithBorder = new View
{
Text = "X",
BorderStyle = LineStyle.Single,
X = 1,
Y = 0,
Width = 3,
Height = 3
};
superView.Add (viewWithBorder);
app.Begin (superView);
DriverAssert.AssertDriverContentsAre (
"""
<EFBFBD>🍎
<EFBFBD>X🍎
<EFBFBD>🍎
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┌─┐🍎<E29490>│X│🍎<E29482>└─┘🍎",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
}
[Fact]
public void Draw_WithBorderSubView_At_Col3_In_WideGlyph_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (6, 3); // Screen: 6 cols wide, 3 rows high; enough for 3x3 border subview at col 3 plus content on the left
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill (),
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
View? view = s as View;
view?.AddStr (0, 0, "🍎🍎🍎🍎");
view?.AddStr (0, 1, "🍎🍎🍎🍎");
view?.AddStr (0, 2, "🍎🍎🍎🍎");
e.DrawContext?.AddDrawnRectangle (view!.Viewport);
e.Cancel = true;
};
// Minimal border at X=3 (odd column), Width=3, Height=3 (includes border)
var viewWithBorder = new View
{
Text = "X",
BorderStyle = LineStyle.Single,
X = 3,
Y = 0,
Width = 3,
Height = 3
};
superView.Add (viewWithBorder);
app.Begin (superView);
DriverAssert.AssertDriverContentsAre (
"""
🍎<EFBFBD>
🍎<EFBFBD>X
🍎<EFBFBD>
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎<6D>┌─┐🍎<E29490>│X│🍎<E29482>└─┘",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
}
[Fact]
public void Draw_NonVisibleView_DoesNotUpdateClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var originalClip = new Region (driver.Screen);
driver.Clip = originalClip.Clone ();
@@ -522,8 +787,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void ExcludeFromClip_ExcludesRegion ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -542,13 +807,12 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.NotNull (driver.Clip);
Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip
}
[Fact]
public void ExcludeFromClip_WithNullClip_DoesNotThrow ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
driver.Clip = null!;
var view = new View
@@ -560,10 +824,9 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Driver = driver
};
var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
Assert.Null (exception);
}
#endregion
@@ -573,7 +836,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_SetsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var view = new View
{
@@ -584,7 +847,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Driver = driver
};
var newClip = new Region (new Rectangle (5, 5, 30, 30));
var newClip = new Region (new (5, 5, 30, 30));
view.SetClip (newClip);
Assert.Equal (newClip, driver.Clip);
@@ -593,8 +856,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact (Skip = "See BUGBUG in SetClip")]
public void SetClip_WithNullClip_ClearsClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (10, 10, 20, 20));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (10, 10, 20, 20));
var view = new View
{
@@ -613,7 +876,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_Excludes_View_From_Clip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var originalClip = new Region (driver.Screen);
driver.Clip = originalClip.Clone ();
@@ -641,8 +904,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_EmptyViewport_DoesNotCrash ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -652,13 +915,13 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Height = 1,
Driver = driver
};
view.Border!.Thickness = new Thickness (1);
view.Border!.Thickness = new (1);
view.BeginInit ();
view.EndInit ();
view.LayoutSubViews ();
// With border of 1, viewport should be empty (0x0 or negative)
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -666,8 +929,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_VeryLargeView_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -681,7 +944,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -689,8 +952,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_NegativeCoordinates_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -704,7 +967,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -712,8 +975,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_OutOfScreenBounds_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -727,7 +990,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}