Skip to content

Commit f548059

Browse files
tigCopilot
andauthored
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 <[email protected]> * Update Terminal.Gui/Drivers/OutputBufferImpl.cs Co-authored-by: Copilot <[email protected]> * Update Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs Co-authored-by: Copilot <[email protected]> * Update Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs Co-authored-by: Copilot <[email protected]> * 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 <[email protected]>
1 parent 5da7e59 commit f548059

File tree

20 files changed

+1037
-315
lines changed

20 files changed

+1037
-315
lines changed

Examples/UICatalog/Scenarios/Keys.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public override void Main ()
158158
appKeyListView.SchemeName = "Runnable";
159159
win.Add (onSwallowedListView);
160160

161-
Application.Driver!.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b", "Esc")); };
161+
Application.Driver!.GetInputProcessor ().AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b", "Esc")); };
162162

163163
Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down");
164164
Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up");

Examples/UICatalog/Scenarios/WideGlyphs.cs

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public override void Main ()
2525
};
2626

2727
// Build the array of codepoints once when subviews are laid out
28-
appWindow.SubViewsLaidOut += (s, e) =>
28+
appWindow.SubViewsLaidOut += (s, _) =>
2929
{
3030
View? view = s as View;
3131
if (view is null)
@@ -34,8 +34,8 @@ public override void Main ()
3434
}
3535

3636
// Only rebuild if size changed or array is null
37-
if (_codepoints is null ||
38-
_codepoints.GetLength (0) != view.Viewport.Height ||
37+
if (_codepoints is null ||
38+
_codepoints.GetLength (0) != view.Viewport.Height ||
3939
_codepoints.GetLength (1) != view.Viewport.Width)
4040
{
4141
_codepoints = new Rune [view.Viewport.Height, view.Viewport.Width];
@@ -51,7 +51,9 @@ public override void Main ()
5151
};
5252

5353
// Fill the window with the pre-built codepoints array
54-
appWindow.DrawingContent += (s, e) =>
54+
// For detailed documentation on the draw code flow from Application.Run to this event,
55+
// see WideGlyphs.DrawFlow.md in this directory
56+
appWindow.DrawingContent += (s, _) =>
5557
{
5658
View? view = s as View;
5759
if (view is null || _codepoints is null)
@@ -73,15 +75,15 @@ public override void Main ()
7375
}
7476
};
7577

76-
Line verticalLineAtEven = new Line ()
78+
Line verticalLineAtEven = new ()
7779
{
7880
X = 10,
7981
Orientation = Orientation.Vertical,
8082
Length = Dim.Fill ()
8183
};
8284
appWindow.Add (verticalLineAtEven);
8385

84-
Line verticalLineAtOdd = new Line ()
86+
Line verticalLineAtOdd = new ()
8587
{
8688
X = 25,
8789
Orientation = Orientation.Vertical,
@@ -97,8 +99,12 @@ public override void Main ()
9799
Y = 5,
98100
Width = 15,
99101
Height = 5,
100-
BorderStyle = LineStyle.Dashed,
102+
//BorderStyle = LineStyle.Dashed,
101103
};
104+
105+
// Proves it's not LineCanvas related
106+
arrangeableViewAtEven!.Border!.Thickness = new (1);
107+
arrangeableViewAtEven.Border.Add(new View () { Height = Dim.Auto(), Width = Dim.Auto(), Text = "Even" });
102108
appWindow.Add (arrangeableViewAtEven);
103109

104110
View arrangeableViewAtOdd = new ()
@@ -112,6 +118,70 @@ public override void Main ()
112118
BorderStyle = LineStyle.Dashed,
113119
};
114120
appWindow.Add (arrangeableViewAtOdd);
121+
122+
var superView = new View
123+
{
124+
CanFocus = true,
125+
X = 30, // on an even column to start
126+
Y = Pos.Center (),
127+
Width = Dim.Auto () + 4,
128+
Height = Dim.Auto () + 1,
129+
BorderStyle = LineStyle.Single,
130+
Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable
131+
};
132+
133+
Rune codepoint = Glyphs.Apple;
134+
135+
superView.DrawingContent += (s, e) =>
136+
{
137+
var view = s as View;
138+
for (var r = 0; r < view!.Viewport.Height; r++)
139+
{
140+
for (var c = 0; c < view.Viewport.Width; c += 2)
141+
{
142+
if (codepoint != default (Rune))
143+
{
144+
view.AddRune (c, r, codepoint);
145+
}
146+
}
147+
}
148+
e.DrawContext?.AddDrawnRectangle (view.Viewport);
149+
e.Cancel = true;
150+
};
151+
appWindow.Add (superView);
152+
153+
var viewWithBorderAtX0 = new View
154+
{
155+
Text = "viewWithBorderAtX0",
156+
BorderStyle = LineStyle.Dashed,
157+
X = 0,
158+
Y = 1,
159+
Width = Dim.Auto (),
160+
Height = 3
161+
};
162+
163+
var viewWithBorderAtX1 = new View
164+
{
165+
Text = "viewWithBorderAtX1",
166+
BorderStyle = LineStyle.Dashed,
167+
X = 1,
168+
Y = Pos.Bottom (viewWithBorderAtX0) + 1,
169+
Width = Dim.Auto (),
170+
Height = 3
171+
};
172+
173+
var viewWithBorderAtX2 = new View
174+
{
175+
Text = "viewWithBorderAtX2",
176+
BorderStyle = LineStyle.Dashed,
177+
X = 2,
178+
Y = Pos.Bottom (viewWithBorderAtX1) + 1,
179+
Width = Dim.Auto (),
180+
Height = 3
181+
};
182+
183+
superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2);
184+
115185
// Run - Start the application.
116186
Application.Run (appWindow);
117187
appWindow.Dispose ();
@@ -124,6 +194,6 @@ private Rune GetRandomWideCodepoint ()
124194
{
125195
Random random = new ();
126196
int codepoint = random.Next (0x4E00, 0x9FFF);
127-
return new Rune (codepoint);
197+
return new (codepoint);
128198
}
129199
}

Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute
109109
/// <inheritdoc />
110110
protected override void Write (StringBuilder output)
111111
{
112+
base.Write (output);
112113
try
113114
{
114115
Console.Out.Write (output);
@@ -140,7 +141,7 @@ protected override bool SetCursorPositionImpl (int col, int row)
140141
}
141142
catch (Exception)
142143
{
143-
return false;
144+
return true;
144145
}
145146
}
146147

Terminal.Gui/Drivers/DriverImpl.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,19 @@ public DriverImpl (
4545
ISizeMonitor sizeMonitor
4646
)
4747
{
48-
InputProcessor = inputProcessor;
48+
_inputProcessor = inputProcessor;
4949
_output = output;
5050
OutputBuffer = outputBuffer;
5151
_ansiRequestScheduler = ansiRequestScheduler;
5252

53-
InputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e);
54-
InputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e);
53+
GetInputProcessor ().KeyDown += (s, e) => KeyDown?.Invoke (s, e);
54+
GetInputProcessor ().KeyUp += (s, e) => KeyUp?.Invoke (s, e);
5555

56-
InputProcessor.MouseEvent += (s, e) =>
57-
{
58-
//Logging.Logger.LogTrace ($"Mouse {e.Flags} at x={e.ScreenPosition.X} y={e.ScreenPosition.Y}");
59-
MouseEvent?.Invoke (s, e);
60-
};
56+
GetInputProcessor ().MouseEvent += (s, e) =>
57+
{
58+
//Logging.Logger.LogTrace ($"Mouse {e.Flags} at x={e.ScreenPosition.X} y={e.ScreenPosition.Y}");
59+
MouseEvent?.Invoke (s, e);
60+
};
6161

6262
SizeMonitor = sizeMonitor;
6363
SizeMonitor.SizeChanged += OnSizeMonitorOnSizeChanged;
@@ -73,15 +73,18 @@ ISizeMonitor sizeMonitor
7373
public void Init () { throw new NotSupportedException (); }
7474

7575
/// <inheritdoc/>
76-
public void Refresh () { _output.Write (OutputBuffer); }
76+
public void Refresh ()
77+
{
78+
_output.Write (OutputBuffer);
79+
}
7780

7881
/// <inheritdoc/>
79-
public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant ();
82+
public string? GetName () => GetInputProcessor ().DriverName?.ToLowerInvariant ();
8083

8184
/// <inheritdoc/>
8285
public virtual string GetVersionInfo ()
8386
{
84-
string type = InputProcessor.DriverName ?? throw new ArgumentNullException (nameof (InputProcessor.DriverName));
87+
string type = GetInputProcessor ().DriverName ?? throw new InvalidOperationException ("Driver name is not set.");
8588

8689
return type;
8790
}
@@ -143,8 +146,12 @@ public void Dispose ()
143146

144147
private readonly IOutput _output;
145148

149+
public IOutput GetOutput () => _output;
150+
151+
private readonly IInputProcessor _inputProcessor;
152+
146153
/// <inheritdoc/>
147-
public IInputProcessor InputProcessor { get; }
154+
public IInputProcessor GetInputProcessor () => _inputProcessor;
148155

149156
/// <inheritdoc/>
150157
public IOutputBuffer OutputBuffer { get; }
@@ -157,7 +164,7 @@ public void Dispose ()
157164

158165
private void CreateClipboard ()
159166
{
160-
if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake"))
167+
if (GetInputProcessor ().DriverName is { } && GetInputProcessor ()!.DriverName!.Contains ("fake"))
161168
{
162169
if (Clipboard is null)
163170
{
@@ -414,7 +421,7 @@ public bool SetCursorVisibility (CursorVisibility visibility)
414421
public event EventHandler<Key>? KeyUp;
415422

416423
/// <inheritdoc/>
417-
public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); }
424+
public void EnqueueKeyEvent (Key key) { GetInputProcessor ().EnqueueKeyDownEvent (key); }
418425

419426
#endregion Input Events
420427

Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,29 @@ namespace Terminal.Gui.Drivers;
77
/// </summary>
88
public class FakeOutput : OutputBase, IOutput
99
{
10-
private readonly StringBuilder _output = new ();
10+
// private readonly StringBuilder _outputStringBuilder = new ();
1111
private int _cursorLeft;
1212
private int _cursorTop;
1313
private Size _consoleSize = new (80, 25);
14+
private IOutputBuffer? _lastBuffer;
1415

1516
/// <summary>
1617
///
1718
/// </summary>
1819
public FakeOutput ()
1920
{
20-
LastBuffer = new OutputBufferImpl ();
21-
LastBuffer.SetSize (80, 25);
21+
_lastBuffer = new OutputBufferImpl ();
22+
_lastBuffer.SetSize (80, 25);
2223
}
2324

2425
/// <summary>
25-
/// Gets or sets the last output buffer written.
26+
/// Gets or sets the last output buffer written. The <see cref="IOutputBuffer.Contents"/> contains
27+
/// a reference to the buffer last written with <see cref="Write(IOutputBuffer)"/>.
2628
/// </summary>
27-
public IOutputBuffer? LastBuffer { get; set; }
29+
public IOutputBuffer? GetLastBuffer () => _lastBuffer;
2830

29-
/// <summary>
30-
/// Gets the captured output as a string.
31-
/// </summary>
32-
public string Output => _output.ToString ();
31+
///// <inheritdoc cref="IOutput.GetLastOutput"/>
32+
//public override string GetLastOutput () => _outputStringBuilder.ToString ();
3333

3434
/// <inheritdoc />
3535
public Point GetCursorPosition ()
@@ -61,28 +61,28 @@ protected override bool SetCursorPositionImpl (int col, int row)
6161
/// <inheritdoc/>
6262
public void Write (ReadOnlySpan<char> text)
6363
{
64-
_output.Append (text);
64+
// _outputStringBuilder.Append (text);
6565
}
6666

67-
/// <inheritdoc cref="IDriver"/>
67+
/// <inheritdoc cref="IOutput.Write(IOutputBuffer)"/>
6868
public override void Write (IOutputBuffer buffer)
6969
{
70-
LastBuffer = buffer;
70+
_lastBuffer = buffer;
7171
base.Write (buffer);
7272
}
7373

74+
///// <inheritdoc/>
75+
//protected override void Write (StringBuilder output)
76+
//{
77+
// _outputStringBuilder.Append (output);
78+
//}
79+
7480
/// <inheritdoc cref="IDriver"/>
7581
public override void SetCursorVisibility (CursorVisibility visibility)
7682
{
7783
// Capture but don't act on it in fake output
7884
}
7985

80-
/// <inheritdoc/>
81-
public void Dispose ()
82-
{
83-
// Nothing to dispose
84-
}
85-
8686
/// <inheritdoc/>
8787
protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
8888
{
@@ -123,8 +123,8 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute
123123
}
124124

125125
/// <inheritdoc/>
126-
protected override void Write (StringBuilder output)
126+
public void Dispose ()
127127
{
128-
_output.Append (output);
128+
// Nothing to dispose
129129
}
130130
}

Terminal.Gui/Drivers/IDriver.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ public interface IDriver : IDisposable
6161
/// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
6262
/// and detecting and processing ansi escape sequences.
6363
/// </summary>
64-
IInputProcessor InputProcessor { get; }
64+
IInputProcessor GetInputProcessor ();
65+
66+
/// <summary>
67+
/// Gets the output handler responsible for writing to the terminal.
68+
/// </summary>
69+
IOutput GetOutput ();
6570

6671
/// <summary>Get the operating system clipboard.</summary>
6772
IClipboard? Clipboard { get; }

Terminal.Gui/Drivers/IOutput.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ public interface IOutput : IDisposable
6565
/// <param name="buffer"></param>
6666
void Write (IOutputBuffer buffer);
6767

68+
/// <summary>
69+
/// Gets a string containing the ANSI escape sequences and content most recently written
70+
/// to the terminal via <see cref="Write(IOutputBuffer)"/>
71+
/// </summary>
72+
string GetLastOutput ();
73+
6874
/// <summary>
6975
/// Generates an ANSI escape sequence string representation of the given <paramref name="buffer"/> contents.
7076
/// This is the same output that would be written to the terminal to recreate the current screen contents.

0 commit comments

Comments
 (0)