diff --git a/Examples/UICatalog/Scenarios/WideGlyphs.cs b/Examples/UICatalog/Scenarios/WideGlyphs.cs
new file mode 100644
index 000000000..25e501118
--- /dev/null
+++ b/Examples/UICatalog/Scenarios/WideGlyphs.cs
@@ -0,0 +1,129 @@
+#nullable enable
+
+using System.Text;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("WideGlyphs", "Demonstrates wide glyphs with overlapped views & clipping")]
+[ScenarioCategory ("Unicode")]
+[ScenarioCategory ("Drawing")]
+
+public sealed class WideGlyphs : Scenario
+{
+ private Rune [,]? _codepoints;
+
+ public override void Main ()
+ {
+ // Init
+ Application.Init ();
+
+ // Setup - Create a top-level application window and configure it.
+ Window appWindow = new ()
+ {
+ Title = GetQuitKeyAndName (),
+ BorderStyle = LineStyle.None
+ };
+
+ // Build the array of codepoints once when subviews are laid out
+ appWindow.SubViewsLaidOut += (s, e) =>
+ {
+ View? view = s as View;
+ if (view is null)
+ {
+ return;
+ }
+
+ // Only rebuild if size changed or array is null
+ if (_codepoints is null ||
+ _codepoints.GetLength (0) != view.Viewport.Height ||
+ _codepoints.GetLength (1) != view.Viewport.Width)
+ {
+ _codepoints = new Rune [view.Viewport.Height, view.Viewport.Width];
+
+ for (int r = 0; r < view.Viewport.Height; r++)
+ {
+ for (int c = 0; c < view.Viewport.Width; c += 2)
+ {
+ _codepoints [r, c] = GetRandomWideCodepoint ();
+ }
+ }
+ }
+ };
+
+ // Fill the window with the pre-built codepoints array
+ appWindow.DrawingContent += (s, e) =>
+ {
+ View? view = s as View;
+ if (view is null || _codepoints is null)
+ {
+ return;
+ }
+
+ // Traverse the Viewport, using the pre-built array
+ for (int r = 0; r < view.Viewport.Height && r < _codepoints.GetLength (0); r++)
+ {
+ for (int c = 0; c < view.Viewport.Width && c < _codepoints.GetLength (1); c += 2)
+ {
+ Rune codepoint = _codepoints [r, c];
+ if (codepoint != default (Rune))
+ {
+ view.AddRune (c, r, codepoint);
+ }
+ }
+ }
+ };
+
+ Line verticalLineAtEven = new Line ()
+ {
+ X = 10,
+ Orientation = Orientation.Vertical,
+ Length = Dim.Fill ()
+ };
+ appWindow.Add (verticalLineAtEven);
+
+ Line verticalLineAtOdd = new Line ()
+ {
+ X = 25,
+ Orientation = Orientation.Vertical,
+ Length = Dim.Fill ()
+ };
+ appWindow.Add (verticalLineAtOdd);
+
+ View arrangeableViewAtEven = new ()
+ {
+ CanFocus = true,
+ Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
+ X = 30,
+ Y = 5,
+ Width = 15,
+ Height = 5,
+ BorderStyle = LineStyle.Dashed,
+ };
+ appWindow.Add (arrangeableViewAtEven);
+
+ View arrangeableViewAtOdd = new ()
+ {
+ CanFocus = true,
+ Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
+ X = 31,
+ Y = 11,
+ Width = 15,
+ Height = 5,
+ BorderStyle = LineStyle.Dashed,
+ };
+ appWindow.Add (arrangeableViewAtOdd);
+ // Run - Start the application.
+ Application.Run (appWindow);
+ appWindow.Dispose ();
+
+ // Shutdown - Calling Application.Shutdown is required.
+ Application.Shutdown ();
+ }
+
+ private Rune GetRandomWideCodepoint ()
+ {
+ Random random = new ();
+ int codepoint = random.Next (0x4E00, 0x9FFF);
+ return new Rune (codepoint);
+ }
+}
diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs
index dfa512192..618448b45 100644
--- a/Terminal.Gui/Drivers/OutputBase.cs
+++ b/Terminal.Gui/Drivers/OutputBase.cs
@@ -89,7 +89,7 @@ public abstract class OutputBase
{
if (output.Length > 0)
{
- WriteToConsole (output, ref lastCol, row, ref outputWidth);
+ WriteToConsole (output, ref lastCol, ref outputWidth);
}
else if (lastCol == -1)
{
@@ -112,11 +112,8 @@ public abstract class OutputBase
}
Cell cell = buffer.Contents [row, col];
- AppendCellAnsi (cell, output, ref redrawAttr, ref _redrawTextStyle, cols, ref col);
-
- outputWidth++;
-
buffer.Contents [row, col].IsDirty = false;
+ AppendCellAnsi (cell, output, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth);
}
}
@@ -221,7 +218,8 @@ public abstract class OutputBase
}
Cell cell = buffer.Contents! [row, col];
- AppendCellAnsi (cell, output, ref lastAttr, ref redrawTextStyle, endCol, ref col);
+ int outputWidth = -1;
+ AppendCellAnsi (cell, output, ref lastAttr, ref redrawTextStyle, endCol, ref col, ref outputWidth);
}
// Add newline at end of row if requested
@@ -241,7 +239,8 @@ public abstract class OutputBase
/// The current text style for optimization.
/// The maximum column, used for wide character handling.
/// The current column, updated for wide characters.
- protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? lastAttr, ref TextStyle redrawTextStyle, int maxCol, ref int currentCol)
+ /// The current output width, updated for wide characters.
+ protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? lastAttr, ref TextStyle redrawTextStyle, int maxCol, ref int currentCol, ref int outputWidth)
{
Attribute? attribute = cell.Attribute;
@@ -256,11 +255,13 @@ public abstract class OutputBase
// Add the grapheme
string grapheme = cell.Grapheme;
output.Append (grapheme);
+ outputWidth++;
// Handle wide grapheme
if (grapheme.GetColumns () > 1 && currentCol + 1 < maxCol)
{
currentCol++; // Skip next cell for wide character
+ outputWidth++;
}
}
@@ -280,7 +281,7 @@ public abstract class OutputBase
return output.ToString ();
}
- private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+ private void WriteToConsole (StringBuilder output, ref int lastCol, ref int outputWidth)
{
if (IsLegacyConsole)
{
diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs
index 14893f9e3..1b46d0d75 100644
--- a/Terminal.Gui/ViewBase/View.Drawing.cs
+++ b/Terminal.Gui/ViewBase/View.Drawing.cs
@@ -127,7 +127,7 @@ public partial class View // Drawing APIs
// because they may draw outside the viewport.
SetClip (originalClip);
originalClip = AddFrameToClip ();
- DoRenderLineCanvas ();
+ DoRenderLineCanvas (context);
// ------------------------------------
// Re-draw the border and padding subviews
@@ -672,8 +672,9 @@ public partial class View // Drawing APIs
#region DrawLineCanvas
- private void DoRenderLineCanvas ()
+ private void DoRenderLineCanvas (DrawContext? context)
{
+ // TODO: Add context to OnRenderingLineCanvas
if (!NeedsDraw || OnRenderingLineCanvas ())
{
return;
@@ -681,7 +682,7 @@ public partial class View // Drawing APIs
// TODO: Add event
- RenderLineCanvas ();
+ RenderLineCanvas (context);
}
///
@@ -709,7 +710,8 @@ public partial class View // Drawing APIs
/// of this view's subviews will be rendered. If is
/// false (the default), this method will cause the to be rendered.
///
- public void RenderLineCanvas ()
+ ///
+ public void RenderLineCanvas (DrawContext? context)
{
if (Driver is null)
{
@@ -728,6 +730,9 @@ public partial class View // Drawing APIs
// TODO: #2616 - Support combining sequences that don't normalize
AddStr (p.Value.Value.Grapheme);
+
+ // Add each drawn cell to the context
+ context?.AddDrawnRectangle (new Rectangle (p.Key, new (1, 1)) );
}
}
@@ -759,9 +764,6 @@ public partial class View // Drawing APIs
// Exclude the Border and Padding from the clip
ExcludeFromClip (Border?.Thickness.AsRegion (Border.FrameToScreen ()));
ExcludeFromClip (Padding?.Thickness.AsRegion (Padding.FrameToScreen ()));
-
- // QUESTION: This makes it so that no nesting of transparent views is possible, but is more correct?
- context = new DrawContext ();
}
else
{
diff --git a/Terminal.Gui/Views/GraphView/LegendAnnotation.cs b/Terminal.Gui/Views/GraphView/LegendAnnotation.cs
index d8ac4be8b..3c4a7b1a8 100644
--- a/Terminal.Gui/Views/GraphView/LegendAnnotation.cs
+++ b/Terminal.Gui/Views/GraphView/LegendAnnotation.cs
@@ -1,5 +1,5 @@
#nullable disable
-namespace Terminal.Gui.Views;
+namespace Terminal.Gui.Views;
///
/// Used by to render smbol definitions in a graph, e.g. colors and their meanings
@@ -46,14 +46,16 @@ public class LegendAnnotation : View, IAnnotation
if (!IsInitialized)
{
// BUGBUG: We should be getting a visual role here?
- SetScheme (new() { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default });
+ SetScheme (new () { Normal = Application.Driver?.GetAttribute () ?? Attribute.Default });
graph.Add (this);
}
if (BorderStyle != LineStyle.None)
{
+ // BUGBUG: View code should never call Draw directly. This
+ // BUGBUG: needs to be refactored to decouple.
DrawAdornments ();
- RenderLineCanvas ();
+ RenderLineCanvas (null);
}
var linesDrawn = 0;
diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
index 0b4c84436..1553149ac 100644
--- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
namespace DriverTests;
@@ -154,6 +154,62 @@ public class OutputBaseTests
Assert.Equal (new Point (2, 0), output.GetCursorPosition ());
}
+ [Theory]
+ [InlineData (true)]
+ [InlineData (false)]
+ public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags_Mixed_Graphemes (bool isLegacyConsole)
+ {
+ // Arrange
+ // FakeOutput exposes this because it's in test scope
+ var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
+ IOutputBuffer buffer = output.LastBuffer!;
+ buffer.SetSize (3, 1);
+
+ // Write '🦮' at col 0 and 'A' at col 3; leave col 1 untouched (not dirty)
+ buffer.Move (0, 0);
+ buffer.AddStr ("🦮A");
+
+ // Confirm some dirtiness before to write
+ Assert.True (buffer.Contents! [0, 0].IsDirty);
+ Assert.False (buffer.Contents! [0, 1].IsDirty);
+ Assert.True (buffer.Contents! [0, 2].IsDirty);
+
+ // Act
+ output.Write (buffer);
+
+ Assert.Contains ("🦮", output.Output);
+ Assert.Contains ("A", output.Output);
+
+ // Dirty flags cleared for the written cells
+ Assert.False (buffer.Contents! [0, 0].IsDirty);
+ Assert.False (buffer.Contents! [0, 1].IsDirty);
+ Assert.False (buffer.Contents! [0, 2].IsDirty);
+
+ Assert.Equal (new (0, 0), output.GetCursorPosition ());
+
+ // Now write 'X' at col 1 which replaces with the replacement character the col 0
+ buffer.Move (1, 0);
+ buffer.AddStr ("X");
+
+ // Confirm dirtiness state before to write
+ Assert.True (buffer.Contents! [0, 0].IsDirty);
+ Assert.True (buffer.Contents! [0, 1].IsDirty);
+ Assert.True (buffer.Contents! [0, 2].IsDirty);
+
+ output.Write (buffer);
+
+ Assert.Contains ("�", output.Output);
+ Assert.Contains ("X", output.Output);
+
+ // Dirty flags cleared for the written cells
+ Assert.False (buffer.Contents! [0, 0].IsDirty);
+ Assert.False (buffer.Contents! [0, 1].IsDirty);
+ Assert.False (buffer.Contents! [0, 2].IsDirty);
+
+ // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
+ Assert.Equal (new (0, 0), output.GetCursorPosition ());
+ }
+
[Theory]
[InlineData (true)]
[InlineData (false)]
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs
index 5aa02f176..53086dfd8 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs
+++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs
@@ -240,7 +240,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Point screenPos = new Point (15, 15);
view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single);
- view.RenderLineCanvas ();
+ view.RenderLineCanvas (null);
// Verify the line was drawn (check for horizontal line character)
for (int i = 0; i < 5; i++)
@@ -272,7 +272,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Assert.NotEqual (Rectangle.Empty, view.LineCanvas.Bounds);
- view.RenderLineCanvas ();
+ view.RenderLineCanvas (null);
// LineCanvas should be cleared after rendering
Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds);
@@ -302,7 +302,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Rectangle boundsBefore = view.LineCanvas.Bounds;
- view.RenderLineCanvas ();
+ view.RenderLineCanvas (null);
// LineCanvas should NOT be cleared when SuperViewRendersLineCanvas is true
Assert.Equal (boundsBefore, view.LineCanvas.Bounds);