mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
129
Examples/UICatalog/Scenarios/WideGlyphs.cs
Normal file
129
Examples/UICatalog/Scenarios/WideGlyphs.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ public abstract class OutputBase
|
|||||||
{
|
{
|
||||||
if (output.Length > 0)
|
if (output.Length > 0)
|
||||||
{
|
{
|
||||||
WriteToConsole (output, ref lastCol, row, ref outputWidth);
|
WriteToConsole (output, ref lastCol, ref outputWidth);
|
||||||
}
|
}
|
||||||
else if (lastCol == -1)
|
else if (lastCol == -1)
|
||||||
{
|
{
|
||||||
@@ -112,11 +112,8 @@ public abstract class OutputBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cell cell = buffer.Contents [row, col];
|
Cell cell = buffer.Contents [row, col];
|
||||||
AppendCellAnsi (cell, output, ref redrawAttr, ref _redrawTextStyle, cols, ref col);
|
|
||||||
|
|
||||||
outputWidth++;
|
|
||||||
|
|
||||||
buffer.Contents [row, col].IsDirty = false;
|
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];
|
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
|
// Add newline at end of row if requested
|
||||||
@@ -241,7 +239,8 @@ public abstract class OutputBase
|
|||||||
/// <param name="redrawTextStyle">The current text style for optimization.</param>
|
/// <param name="redrawTextStyle">The current text style for optimization.</param>
|
||||||
/// <param name="maxCol">The maximum column, used for wide character handling.</param>
|
/// <param name="maxCol">The maximum column, used for wide character handling.</param>
|
||||||
/// <param name="currentCol">The current column, updated for wide characters.</param>
|
/// <param name="currentCol">The current column, updated for wide characters.</param>
|
||||||
protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? lastAttr, ref TextStyle redrawTextStyle, int maxCol, ref int currentCol)
|
/// <param name="outputWidth">The current output width, updated for wide characters.</param>
|
||||||
|
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;
|
Attribute? attribute = cell.Attribute;
|
||||||
|
|
||||||
@@ -256,11 +255,13 @@ public abstract class OutputBase
|
|||||||
// Add the grapheme
|
// Add the grapheme
|
||||||
string grapheme = cell.Grapheme;
|
string grapheme = cell.Grapheme;
|
||||||
output.Append (grapheme);
|
output.Append (grapheme);
|
||||||
|
outputWidth++;
|
||||||
|
|
||||||
// Handle wide grapheme
|
// Handle wide grapheme
|
||||||
if (grapheme.GetColumns () > 1 && currentCol + 1 < maxCol)
|
if (grapheme.GetColumns () > 1 && currentCol + 1 < maxCol)
|
||||||
{
|
{
|
||||||
currentCol++; // Skip next cell for wide character
|
currentCol++; // Skip next cell for wide character
|
||||||
|
outputWidth++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +281,7 @@ public abstract class OutputBase
|
|||||||
return output.ToString ();
|
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)
|
if (IsLegacyConsole)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ public partial class View // Drawing APIs
|
|||||||
// because they may draw outside the viewport.
|
// because they may draw outside the viewport.
|
||||||
SetClip (originalClip);
|
SetClip (originalClip);
|
||||||
originalClip = AddFrameToClip ();
|
originalClip = AddFrameToClip ();
|
||||||
DoRenderLineCanvas ();
|
DoRenderLineCanvas (context);
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Re-draw the border and padding subviews
|
// Re-draw the border and padding subviews
|
||||||
@@ -672,8 +672,9 @@ public partial class View // Drawing APIs
|
|||||||
|
|
||||||
#region DrawLineCanvas
|
#region DrawLineCanvas
|
||||||
|
|
||||||
private void DoRenderLineCanvas ()
|
private void DoRenderLineCanvas (DrawContext? context)
|
||||||
{
|
{
|
||||||
|
// TODO: Add context to OnRenderingLineCanvas
|
||||||
if (!NeedsDraw || OnRenderingLineCanvas ())
|
if (!NeedsDraw || OnRenderingLineCanvas ())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -681,7 +682,7 @@ public partial class View // Drawing APIs
|
|||||||
|
|
||||||
// TODO: Add event
|
// TODO: Add event
|
||||||
|
|
||||||
RenderLineCanvas ();
|
RenderLineCanvas (context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -709,7 +710,8 @@ public partial class View // Drawing APIs
|
|||||||
/// <see cref="LineCanvas"/> of this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is
|
/// <see cref="LineCanvas"/> of this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is
|
||||||
/// false (the default), this method will cause the <see cref="LineCanvas"/> to be rendered.
|
/// false (the default), this method will cause the <see cref="LineCanvas"/> to be rendered.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RenderLineCanvas ()
|
/// <param name="context"></param>
|
||||||
|
public void RenderLineCanvas (DrawContext? context)
|
||||||
{
|
{
|
||||||
if (Driver is null)
|
if (Driver is null)
|
||||||
{
|
{
|
||||||
@@ -728,6 +730,9 @@ public partial class View // Drawing APIs
|
|||||||
|
|
||||||
// TODO: #2616 - Support combining sequences that don't normalize
|
// TODO: #2616 - Support combining sequences that don't normalize
|
||||||
AddStr (p.Value.Value.Grapheme);
|
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
|
// Exclude the Border and Padding from the clip
|
||||||
ExcludeFromClip (Border?.Thickness.AsRegion (Border.FrameToScreen ()));
|
ExcludeFromClip (Border?.Thickness.AsRegion (Border.FrameToScreen ()));
|
||||||
ExcludeFromClip (Padding?.Thickness.AsRegion (Padding.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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
namespace Terminal.Gui.Views;
|
namespace Terminal.Gui.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used by <see cref="GraphView"/> to render smbol definitions in a graph, e.g. colors and their meanings
|
/// Used by <see cref="GraphView"/> to render smbol definitions in a graph, e.g. colors and their meanings
|
||||||
@@ -46,14 +46,16 @@ public class LegendAnnotation : View, IAnnotation
|
|||||||
if (!IsInitialized)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
// BUGBUG: We should be getting a visual role here?
|
// 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);
|
graph.Add (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BorderStyle != LineStyle.None)
|
if (BorderStyle != LineStyle.None)
|
||||||
{
|
{
|
||||||
|
// BUGBUG: View code should never call Draw directly. This
|
||||||
|
// BUGBUG: needs to be refactored to decouple.
|
||||||
DrawAdornments ();
|
DrawAdornments ();
|
||||||
RenderLineCanvas ();
|
RenderLineCanvas (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var linesDrawn = 0;
|
var linesDrawn = 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace DriverTests;
|
namespace DriverTests;
|
||||||
|
|
||||||
@@ -154,6 +154,62 @@ public class OutputBaseTests
|
|||||||
Assert.Equal (new Point (2, 0), output.GetCursorPosition ());
|
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 ("<22>", 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]
|
[Theory]
|
||||||
[InlineData (true)]
|
[InlineData (true)]
|
||||||
[InlineData (false)]
|
[InlineData (false)]
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
|
|||||||
Point screenPos = new Point (15, 15);
|
Point screenPos = new Point (15, 15);
|
||||||
view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single);
|
view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single);
|
||||||
|
|
||||||
view.RenderLineCanvas ();
|
view.RenderLineCanvas (null);
|
||||||
|
|
||||||
// Verify the line was drawn (check for horizontal line character)
|
// Verify the line was drawn (check for horizontal line character)
|
||||||
for (int i = 0; i < 5; i++)
|
for (int i = 0; i < 5; i++)
|
||||||
@@ -272,7 +272,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
|
|||||||
|
|
||||||
Assert.NotEqual (Rectangle.Empty, view.LineCanvas.Bounds);
|
Assert.NotEqual (Rectangle.Empty, view.LineCanvas.Bounds);
|
||||||
|
|
||||||
view.RenderLineCanvas ();
|
view.RenderLineCanvas (null);
|
||||||
|
|
||||||
// LineCanvas should be cleared after rendering
|
// LineCanvas should be cleared after rendering
|
||||||
Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds);
|
Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds);
|
||||||
@@ -302,7 +302,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
|
|||||||
|
|
||||||
Rectangle boundsBefore = view.LineCanvas.Bounds;
|
Rectangle boundsBefore = view.LineCanvas.Bounds;
|
||||||
|
|
||||||
view.RenderLineCanvas ();
|
view.RenderLineCanvas (null);
|
||||||
|
|
||||||
// LineCanvas should NOT be cleared when SuperViewRendersLineCanvas is true
|
// LineCanvas should NOT be cleared when SuperViewRendersLineCanvas is true
|
||||||
Assert.Equal (boundsBefore, view.LineCanvas.Bounds);
|
Assert.Equal (boundsBefore, view.LineCanvas.Bounds);
|
||||||
|
|||||||
Reference in New Issue
Block a user