Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Examples/UICatalog/ScenarioLogCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace UICatalog;
/// </summary>
public class ScenarioLogCapture : ILoggerProvider
{
private const int MaxBufferChars = 256_000;
private const int TrimTargetChars = 192_000;

private readonly object _lock = new ();
private readonly StringBuilder _buffer = new ();

Expand Down Expand Up @@ -113,6 +116,7 @@ internal void Log (LogLevel logLevel, string message)
lock (_lock)
{
_buffer.AppendLine ($"[{logLevel}] {message}");
TrimIfNeeded ();

if (logLevel >= LogLevel.Error)
{
Expand All @@ -121,6 +125,40 @@ internal void Log (LogLevel logLevel, string message)
}
}

private void TrimIfNeeded ()
{
if (_buffer.Length <= MaxBufferChars)
{
return;
}

var removeCount = _buffer.Length - TrimTargetChars;

if (removeCount <= 0)
{
return;
}

int nextLineBreak = -1;

for (int i = removeCount; i < _buffer.Length; i++)
{
if (_buffer [i] == '\n')
{
nextLineBreak = i;
break;
}
}

if (nextLineBreak >= 0)
{
removeCount = nextLineBreak + 1;
Comment thread
tig marked this conversation as resolved.
}

_buffer.Remove (0, removeCount);
_scenarioStartPosition = Math.Max (0, _scenarioStartPosition - removeCount);
}

/// <inheritdoc />
public void Dispose ()
{
Expand Down
6 changes: 6 additions & 0 deletions Terminal.Gui/ViewBase/View.Drawing.Adornments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ public void DrawAdornments ()
}
else if (Padding.Thickness != Thickness.Empty)
{
// Thickness.Draw fills via the driver's CurrentAttribute and does not set one itself, so
// set the view's Normal here. Otherwise the padding inherits whatever attribute was last
// used — which is stale (e.g. a sibling raster view's Color.None) when the parent skipped
// its ClearViewport on a child-only redraw, bleeding foreign content into the padding
// column. (gui-cs/Terminal.Gui#5518.)
SetAttributeForRole (VisualRole.Normal);
Padding.Thickness.Draw (Driver, Padding.FrameToScreen (), Padding.Diagnostics);
Padding.LastDrawnRegion = null;
}
Expand Down
66 changes: 66 additions & 0 deletions Tests/IntegrationTests/ScenarioLogCaptureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Extensions.Logging;
using UICatalog;

namespace IntegrationTests;

public class ScenarioLogCaptureTests
{
[Fact]
public void LogBuffer_IsTrimmed_WhenItGrowsTooLarge ()
{
ScenarioLogCapture capture = new ();
ILogger logger = capture.CreateLogger ("test");
string message = new ('x', 4096);

for (var i = 0; i < 120; i++)
{
logger.LogInformation ("{Message}", message);
}

Assert.True (capture.GetAllLogs ().Length <= 256_000);
}

[Fact]
public void GetScenarioLogs_RespectsScenarioStart_AfterTrim ()
{
ScenarioLogCapture capture = new ();
ILogger logger = capture.CreateLogger ("test");
string message = new ('y', 4096);

// Fill close to the cap so the next few entries will trigger a trim.
while (capture.GetAllLogs ().Length < 250_000)
{
logger.LogInformation ("{Message}", message);
}

logger.LogInformation ("before-start");

capture.MarkScenarioStart ();

int lengthBefore = capture.GetAllLogs ().Length;
bool trimmed = false;

for (int i = 0; i < 50; i++)
{
logger.LogInformation ("{Message}", message);

int lengthAfter = capture.GetAllLogs ().Length;

if (lengthAfter < lengthBefore)
{
trimmed = true;
break;
}

lengthBefore = lengthAfter;
}

Assert.True (trimmed);

logger.LogInformation ("after-start");

string logs = capture.GetScenarioLogs ();
Assert.DoesNotContain ("before-start", logs);
Assert.Contains ("after-start", logs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace ViewsTests;

/// <summary>
/// Regression for gui-cs/Terminal.Gui#5518.
/// <para>
/// A raster <see cref="ImageView"/> placed at <see cref="Pos.Right"/> of a view that has a right
/// <see cref="Adornment"/> padding column overwrites that padding column — the cell one to the
/// LEFT of the ImageView's frame — when it redraws. The full first draw repaints the neighbour so
/// it looks correct; a <em>partial</em> redraw (a focus round-trip here) leaves the overwrite
/// behind, bleeding the image pane's background into the neighbour.
/// </para>
/// <para>
/// Surfaced in winprint (gui-cs/Terminal.Gui#5518) as the page-preview pane bleeding its canvas
/// colour into the settings panel's seam, on both Sixel and Kitty. It is not actually
/// raster-specific — a plain <see cref="View"/> at <see cref="Pos.Right"/> reproduces it too — so
/// the fix most likely belongs in the View draw/clip path, not the raster code.
/// </para>
/// </summary>
public class ImageViewNeighbourPaddingTests
{
[Fact]
public void Draw_RightOfPaddedNeighbour_DoesNotOverwriteNeighboursPaddingColumn ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.Driver!.SetScreenSize (40, 12);

Runnable runnable = new () { Width = 40, Height = 12 };
runnable.SetScheme (new Scheme { Normal = new Attribute (new Color (255, 255, 255), new Color (26, 26, 26)) });
app.Begin (runnable);

DriverImpl driver = (DriverImpl)app.Driver!;
driver.SetSixelSupport (new SixelSupportResult { IsSupported = true, Resolution = new Size (9, 20), MaxPaletteColors = 256 });

// Left pane with a one-column right Padding — that padding column is the "seam".
View left = new () { X = 0, Y = 0, Width = 10, Height = Dim.Fill (), CanFocus = true };
left.Padding!.Thickness = new Thickness (0, 0, 1, 0);
left.SetScheme (new Scheme { Normal = new Attribute (new Color (255, 255, 255), new Color (200, 0, 0)) });

// Raster ImageView immediately to the right of the seam.
Color gray = new (224, 224, 224);
ImageView img = new () { X = Pos.Right (left), Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true };
img.SetScheme (new Scheme { Normal = new Attribute (new Color (0, 0, 0), gray) });
img.Image = CreateSolidImage (90, 200, gray);

// ImageView added first, neighbour on top: the full first draw repaints the seam correctly.
runnable.Add (img, left);

img.SetFocus ();
app.LayoutAndDraw ();
app.LayoutAndDraw ();

int seam = img.ViewportToScreen ().X - 1; // the neighbour's padding column, outside the image frame
int row = img.ViewportToScreen ().Y + 2;

Attribute? before = driver.GetOutputBuffer ().Contents! [row, seam].Attribute;

// Partial redraw: focus the neighbour, then focus back to the image.
left.SetFocus ();
app.LayoutAndDraw ();
img.SetFocus ();
app.LayoutAndDraw ();

Attribute? after = driver.GetOutputBuffer ().Contents! [row, seam].Attribute;

// The ImageView must not have changed a cell outside (to the left of) its own frame.
Assert.Equal (before, after);

runnable.Dispose ();
}

private static Color [,] CreateSolidImage (int width, int height, Color color)
{
Color [,] image = new Color [width, height];

for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
image [x, y] = color;
}
}

return image;
}
}
Loading