diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 79198349f1..315b2bbc06 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -67,6 +67,28 @@ public static string ToString (IConsoleDriver? driver) { sb.Append (sp); } + else if (contents [r, c].CombiningMarks is { Count: > 0 }) + { + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[ ]`. + // + // For now, we just ignore the list of CMs. + string combine = rune.ToString (); + string? normalized = null; + + foreach (Rune combMark in contents [r, c].CombiningMarks) + { + combine += combMark; + normalized = combine.Normalize (NormalizationForm.FormC); + } + + foreach (Rune enumerateRune in normalized!.EnumerateRunes ()) + { + sb.Append (enumerateRune); + } + } else { sb.Append ((char)rune.Value); @@ -76,11 +98,6 @@ public static string ToString (IConsoleDriver? driver) { c++; } - - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // sb.Append ((char)combMark.Value); - //} } sb.AppendLine (); diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 2ba8ba6018..462ff09c16 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics; +using System.Globalization; namespace Terminal.Gui; @@ -54,6 +55,10 @@ public abstract class ConsoleDriver : IConsoleDriver // This is in addition to the dirty flag on each cell. internal bool []? _dirtyLines; + // Represent the necessary space character that must be added at + // the end of the line due the use of Format (Cf) unicode category + internal int []? _lineColsOffset; + // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// Gets the location and size of the terminal screen. public Rectangle Screen => new (0, 0, Cols, Rows); @@ -169,6 +174,13 @@ public virtual int Rows /// The topmost row in the terminal. public virtual int Top { get; set; } = 0; + /// + /// Gets or sets whenever is ignored or not. + /// + public static bool IgnoreIsCombiningMark { get; set; } + + private Point? _lastValidAddRuneCell = null; + /// Adds the specified rune to the display at the current cursor position. /// /// @@ -185,15 +197,15 @@ public virtual int Rows /// Rune to add. public void AddRune (Rune rune) { - int runeWidth = -1; - bool validLocation = IsValidLocation (rune, Col, Row); - if (Contents is null) { return; } + int runeWidth = -1; + bool validLocation = IsValidLocation (rune, Col, Row); Rectangle clipRect = Clip!.GetBounds (); + bool wasAddedToCombiningMarks = false; if (validLocation) { @@ -202,7 +214,7 @@ public void AddRune (Rune rune) lock (Contents) { - if (runeWidth == 0 && rune.IsCombiningMark ()) + if (Rune.GetUnicodeCategory (rune) == UnicodeCategory.Format || (runeWidth == 0 && rune.IsCombiningMark ())) { // AtlasEngine does not support NON-NORMALIZED combining marks in a way // compatible with the driver architecture. Any CMs (except in the first col) @@ -212,42 +224,27 @@ public void AddRune (Rune rune) // Until this is addressed (see Issue #), we do our best by // a) Attempting to normalize any CM with the base char to it's left // b) Ignoring any CMs that don't normalize - if (Col > 0) + if (Col > 0 + && _lastValidAddRuneCell is { } + && _lastValidAddRuneCell.Value.Y == Row + && Contents [Row, _lastValidAddRuneCell.Value.X].IsDirty) { - if (Contents [Row, Col - 1].CombiningMarks.Count > 0) + if (Contents [Row, _lastValidAddRuneCell.Value.X].CombiningMarks is null) { - // Just add this mark to the list - Contents [Row, Col - 1].CombiningMarks.Add (rune); - - // Ignore. Don't move to next column (let the driver figure out what to do). + Contents [Row, _lastValidAddRuneCell.Value.X].CombiningMarks = []; } - else - { - // Attempt to normalize the cell to our left combined with this mark - string combined = Contents [Row, Col - 1].Rune + rune.ToString (); - // Normalize to Form C (Canonical Composition) - string normalized = combined.Normalize (NormalizationForm.FormC); - - if (normalized.Length == 1) - { - // It normalized! We can just set the Cell to the left with the - // normalized codepoint - Contents [Row, Col - 1].Rune = (Rune)normalized [0]; + // Just add this mark to the list + Contents [Row, _lastValidAddRuneCell.Value.X].CombiningMarks.Add (rune); + wasAddedToCombiningMarks = true; - // Ignore. Don't move to next column because we're already there - } - else - { - // It didn't normalize. Add it to the Cell to left's CM list - Contents [Row, Col - 1].CombiningMarks.Add (rune); - - // Ignore. Don't move to next column (let the driver figure out what to do). - } + if (runeWidth == 0 && Rune.GetUnicodeCategory (rune) == UnicodeCategory.Format) + { + _lineColsOffset! [Row] += Contents [Row, _lastValidAddRuneCell.Value.X].Rune.GetColumns (); } - Contents [Row, Col - 1].Attribute = CurrentAttribute; - Contents [Row, Col - 1].IsDirty = true; + Contents [Row, _lastValidAddRuneCell.Value.X].Attribute = CurrentAttribute; + Contents [Row, _lastValidAddRuneCell.Value.X].IsDirty = true; } else { @@ -255,7 +252,11 @@ public void AddRune (Rune rune) Contents [Row, Col].Rune = rune; Contents [Row, Col].Attribute = CurrentAttribute; Contents [Row, Col].IsDirty = true; - Col++; + + if (runeWidth == 0) + { + Col++; + } } } else @@ -325,7 +326,12 @@ public void AddRune (Rune rune) } } - if (runeWidth is < 0 or > 0) + if (!IgnoreIsCombiningMark && !wasAddedToCombiningMarks) + { + _lastValidAddRuneCell = new (Col, Row); + } + + if (runeWidth is < 0 or > 0 && !wasAddedToCombiningMarks) { Col++; } @@ -339,13 +345,10 @@ public void AddRune (Rune rune) lock (Contents!) { // This is a double-width character, and we are not at the end of the line. - // Col now points to the second column of the character. Ensure it doesn't - // Get rendered. - Contents [Row, Col].IsDirty = false; + // Col now points to the second column of the character. Ensure it does get + // rendered to allow the driver to handle it in its own way. + Contents [Row, Col].IsDirty = true; Contents [Row, Col].Attribute = CurrentAttribute; - - // TODO: Determine if we should wipe this out (for now now) - //Contents [Row, Col].Rune = (Rune)' '; } } @@ -415,12 +418,14 @@ public void FillRect (Rectangle rect, Rune rune = default) public void ClearContents () { Contents = new Cell [Rows, Cols]; + _lastValidAddRuneCell = null; //CONCURRENCY: Unsynchronized access to Clip isn't safe. // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere. Clip = new (Screen); _dirtyLines = new bool [Rows]; + _lineColsOffset = new int [Rows]; lock (Contents) { @@ -499,11 +504,8 @@ public bool IsValidLocation (Rune rune, int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); } - else - { - return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); - } + return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); } /// Called when the terminal size changes. Fires the event. diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 48bcb713f5..51b257e7b6 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -318,9 +318,23 @@ public override bool UpdateScreen () outputWidth++; Rune rune = Contents [row, col].Rune; + + if (rune == Rune.ReplacementChar) + { + continue; + } + output.Append (rune); - if (Contents [row, col].CombiningMarks.Count > 0) + if (rune.IsCombiningMark ()) + { + if (rune.GetColumns () == 0) + { + output.Append (' '); + } + } + + if (Contents [row, col].CombiningMarks is { Count: > 0 }) { // AtlasEngine does not support NON-NORMALIZED combining marks in a way // compatible with the driver architecture. Any CMs (except in the first col) @@ -328,21 +342,32 @@ public override bool UpdateScreen () // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. // // For now, we just ignore the list of CMs. - //foreach (var combMark in Contents [row, col].CombiningMarks) { - // output.Append (combMark); - //} - // WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPosition (col - 1, row); + foreach (var combMark in Contents [row, col].CombiningMarks) + { + output.Append (combMark); + } + //WriteToConsole (output, ref lastCol, row, ref outputWidth); } + //else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) + //{ + // WriteToConsole (output, ref lastCol, row, ref outputWidth); + // SetCursorPosition (col - 1, row); + //} Contents [row, col].IsDirty = false; } } + if (_lineColsOffset! [row] > 0) + { + for (var i = 0; i < _lineColsOffset [row]; i++) + { + output.Append (' '); + } + + _lineColsOffset! [row] = 0; + } + if (output.Length > 0) { SetCursorPosition (lastCol, row); diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs index 2ede00a000..140e748555 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -166,9 +166,23 @@ public override bool UpdateScreen () outputWidth++; Rune rune = Contents [row, col].Rune; + + if (rune == Rune.ReplacementChar) + { + continue; + } + output.Append (rune); - if (Contents [row, col].CombiningMarks.Count > 0) + if (rune.IsCombiningMark ()) + { + if (rune.GetColumns () == 0) + { + output.Append (' '); + } + } + + if (Contents [row, col].CombiningMarks is { Count: > 0 }) { // AtlasEngine does not support NON-NORMALIZED combining marks in a way // compatible with the driver architecture. Any CMs (except in the first col) @@ -176,21 +190,32 @@ public override bool UpdateScreen () // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. // // For now, we just ignore the list of CMs. - //foreach (var combMark in Contents [row, col].CombiningMarks) { - // output.Append (combMark); - //} - // WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPosition (col - 1, row); + foreach (var combMark in Contents [row, col].CombiningMarks) + { + output.Append (combMark); + } + //WriteToConsole (output, ref lastCol, row, ref outputWidth); } + //else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) + //{ + // WriteToConsole (output, ref lastCol, row, ref outputWidth); + // SetCursorPosition (col - 1, row); + //} Contents [row, col].IsDirty = false; } } + if (_lineColsOffset! [row] > 0) + { + for (var i = 0; i < _lineColsOffset [row]; i++) + { + output.Append (' '); + } + + _lineColsOffset! [row] = 0; + } + if (output.Length > 0) { SetCursorPosition (lastCol, row); diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs index 37be0766f6..dab711ca67 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs @@ -139,6 +139,11 @@ private void NetInputHandler () _waitForProbe.Reset (); } + if (_inputHandlerTokenSource.IsCancellationRequested) + { + return; + } + ProcessInputQueue (); } catch (OperationCanceledException) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 2c6689737f..abe5a373af 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -10,7 +10,6 @@ internal class WindowsConsole { private CancellationTokenSource? _inputReadyCancellationTokenSource; private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); - internal WindowsMainLoop? _mainLoop; public const int STD_OUTPUT_HANDLE = -11; public const int STD_INPUT_HANDLE = -10; @@ -197,6 +196,22 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord { _stringBuilder.Append (info.Char); } + + if (Rune.IsValid (info.Char) && ((Rune)info.Char).IsCombiningMark ()) + { + if (((Rune)info.Char).GetColumns () == 0) + { + _stringBuilder.Append (' '); + } + } + + if (info.CombiningMarks is { }) + { + foreach (var combMark in info.CombiningMarks) + { + _stringBuilder.Append (combMark); + } + } } else { @@ -785,6 +800,7 @@ public struct ExtendedCharInfo public char Char { get; set; } public Attribute Attribute { get; set; } public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences + internal List? CombiningMarks; public ExtendedCharInfo (char character, Attribute attribute) { diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 02d8a03ff7..cf534d09fa 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -335,6 +335,20 @@ public override bool UpdateScreen () _outputBuffer [position].Empty = false; + if (Contents [row, col].CombiningMarks is { Count: > 0 }) + { + _outputBuffer [position].CombiningMarks = []; + + foreach (var combMark in Contents [row, col].CombiningMarks) + { + _outputBuffer [position].CombiningMarks!.Add ((char)combMark.Value); + } + } + else + { + _outputBuffer [position].CombiningMarks = null; + } + if (Contents [row, col].Rune.IsBmp) { _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value; @@ -342,7 +356,21 @@ public override bool UpdateScreen () else { //_outputBuffer [position].Empty = true; - _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + //_outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + var rune = Contents [row, col].Rune; + char [] surrogatePair = rune.ToString ().ToCharArray (); + Debug.Assert (surrogatePair.Length == 2); + _outputBuffer [position].Char = surrogatePair [0]; + + if (_outputBuffer [position].CombiningMarks == null) + { + _outputBuffer [position].CombiningMarks = []; + _outputBuffer [position].CombiningMarks!.Add (surrogatePair [1]); + } + else + { + _outputBuffer [position].CombiningMarks!.Insert (0, surrogatePair [1]); + } if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols) { @@ -350,7 +378,7 @@ public override bool UpdateScreen () col++; position = row * Cols + col; _outputBuffer [position].Empty = false; - _outputBuffer [position].Char = ' '; + _outputBuffer [position].Char = '\0'; } } } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs index fedcf6f732..f4856fa882 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs @@ -36,7 +36,6 @@ public WindowsMainLoop (IConsoleDriver consoleDriver) if (!ConsoleDriver.RunningUnitTests) { _winConsole = ((WindowsDriver)consoleDriver).WinConsole; - _winConsole!._mainLoop = this; } } diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 5ce4e21df5..d769265609 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -4,7 +4,7 @@ /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. and /// ). /// -public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default) +public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default, List CombiningMarks = null) { /// The attributes to use when drawing the Glyph. public Attribute? Attribute { get; set; } = Attribute; @@ -23,13 +23,11 @@ public Rune Rune get => _rune; set { - CombiningMarks.Clear (); + CombiningMarks = null; _rune = value; } } - private List _combiningMarks; - /// /// The combining marks for that when combined makes this Cell a combining sequence. If /// empty, then is ignored. @@ -38,11 +36,7 @@ public Rune Rune /// Only valid in the rare case where is a combining sequence that could not be normalized to a /// single Rune. /// - internal List CombiningMarks - { - get => _combiningMarks ?? []; - private set => _combiningMarks = value ?? []; - } + internal List CombiningMarks { get; set; } = CombiningMarks; /// public override string ToString () { return $"[{Rune}, {Attribute}]"; } diff --git a/Terminal.Gui/Text/RuneExtensions.cs b/Terminal.Gui/Text/RuneExtensions.cs index 835dba6791..e6912f4c22 100644 --- a/Terminal.Gui/Text/RuneExtensions.cs +++ b/Terminal.Gui/Text/RuneExtensions.cs @@ -129,7 +129,8 @@ public static bool IsCombiningMark (this Rune rune) return Rune.GetUnicodeCategory (rune) == UnicodeCategory.NonSpacingMark || category == UnicodeCategory.SpacingCombiningMark - || category == UnicodeCategory.EnclosingMark; + || category == UnicodeCategory.EnclosingMark + || category == UnicodeCategory.Format; } /// Reports whether a rune is a surrogate code point. diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index b49c8c4e6f..581f2f6efd 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -247,6 +247,8 @@ public bool ShowGlyphWidths /// protected override bool OnDrawingContent () { + ConsoleDriver.IgnoreIsCombiningMark = true; + if (Viewport.Height == 0 || Viewport.Width == 0) { return true; @@ -324,37 +326,37 @@ protected override bool OnDrawingContent () if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0) { // Draw the rune - if (width > 0) - { + //if (width > 0) + //{ AddRune (rune); - } - else - { - if (rune.IsCombiningMark ()) - { - // This is a hack to work around the fact that combining marks - // a) can't be rendered on their own - // b) that don't normalize are not properly supported in - // any known terminal (esp Windows/AtlasEngine). - // See Issue #2616 - var sb = new StringBuilder (); - sb.Append ('a'); - sb.Append (rune); - - // Try normalizing after combining with 'a'. If it normalizes, at least - // it'll show on the 'a'. If not, just show the replacement char. - string normal = sb.ToString ().Normalize (NormalizationForm.FormC); - - if (normal.Length == 1) - { - AddRune ((Rune)normal [0]); - } - else - { - AddRune (Rune.ReplacementChar); - } - } - } + //} + //else + //{ + // if (rune.IsCombiningMark ()) + // { + // // This is a hack to work around the fact that combining marks + // // a) can't be rendered on their own + // // b) that don't normalize are not properly supported in + // // any known terminal (esp Windows/AtlasEngine). + // // See Issue #2616 + // var sb = new StringBuilder (); + // sb.Append ('a'); + // sb.Append (rune); + + // // Try normalizing after combining with 'a'. If it normalizes, at least + // // it'll show on the 'a'. If not, just show the replacement char. + // string normal = sb.ToString ().Normalize (NormalizationForm.FormC); + + // if (normal.Length == 1) + // { + // AddRune ((Rune)normal [0]); + // } + // else + // { + // AddRune (Rune.ReplacementChar); + // } + // } + //} } else { @@ -385,6 +387,8 @@ protected override bool OnDrawingContent () } } + ConsoleDriver.IgnoreIsCombiningMark = false; + return true; } diff --git a/Terminal.Gui/Views/CharMap/UnicodeRange.cs b/Terminal.Gui/Views/CharMap/UnicodeRange.cs index 1880b2671e..baa1c1fac7 100644 --- a/Terminal.Gui/Views/CharMap/UnicodeRange.cs +++ b/Terminal.Gui/Views/CharMap/UnicodeRange.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; /// -/// Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus +/// Represents all the Unicode ranges from System.Text.Unicode.UnicodeRange plus /// the non-BMP ranges not included. /// public class UnicodeRange (int start, int end, string category) diff --git a/UICatalog/Scenarios/CombiningMarks.cs b/UICatalog/Scenarios/CombiningMarks.cs index d351982513..89f178133d 100644 --- a/UICatalog/Scenarios/CombiningMarks.cs +++ b/UICatalog/Scenarios/CombiningMarks.cs @@ -12,22 +12,79 @@ public override void Main () var top = new Toplevel (); top.DrawComplete += (s, e) => - { - top.Move (0, 0); - top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); - top.Move (0, 2); - top.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr."); - top.Move (0, 3); - top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr."); - top.Move (0, 4); - top.AddRune ('['); - top.AddRune ('a'); - top.AddRune ('\u0301'); - top.AddRune ('\u0301'); - top.AddRune ('\u0328'); - top.AddRune (']'); - top.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each."); - }; + { + // Forces reset _lineColsOffset because we're dealing with direct draw + Application.ClearScreenNextIteration = true; + + var i = -1; + top.Move (0, ++i); + top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); + top.Move (0, ++i); + top.AddStr ("\u0301<- \"\\u0301\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u0301]<- \"[\\u0301]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[ \u0301]<- \"[ \\u0301]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u0301 ]<- \"[\\u0301 ]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("\u0301\u0301\u0328<- \"\\u0301\\u0301\\u0328\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u0301\u0301\u0328]<- \"[\\u0301\\u0301\\u0328]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr."); + top.Move (0, ++i); + top.AddRune ('['); + top.AddRune ('a'); + top.AddRune ('\u0301'); + top.AddRune ('\u0301'); + top.AddRune ('\u0328'); + top.AddRune (']'); + top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each."); + top.Move (0, ++i); + top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("\u00ad<- \"\\u00ad\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u00ad]<- \"[\\u00ad]\" using AddStr."); + top.Move (0, ++i); + top.AddRune ('['); + top.AddRune ('\u00ad'); + top.AddRune (']'); + top.AddStr ("<- \"[\\u00ad]\" using AddRune for each."); + i++; + top.Move (0, ++i); + top.AddStr ("From now on we are using TextFormatter"); + TextFormatter tf = new () { Text = "[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using TextFormatter." }; + tf.Draw (new (0, ++i, tf.Text.Length, 1), top.ColorScheme.Normal, top.ColorScheme.Normal); + tf.Text = "[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using TextFormatter."; + tf.Draw (new (0, ++i, tf.Text.Length, 1), top.ColorScheme.Normal, top.ColorScheme.Normal); + i++; + top.Move (0, ++i); + top.AddStr ("From now on we are using Surrogate pairs with combining diacritics"); + top.Move (0, ++i); + top.AddStr ("[\ud835\udc4b\u0302]<- \"[\\ud835\\udc4b\\u0302]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\ud83d\udc68\ud83e\uddd2]<- \"[\\ud83d\\udc68\\ud83e\\uddd2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("\u200d<- \"\\u200d\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\u200d]<- \"[\\u200d]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\ud83d\udc68\u200d\ud83e\uddd2]<- \"[\\ud83d\\udc68\\u200d\\ud83e\\uddd2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F469\U0001F9D2]<- \"[\\U0001F469\\U0001F9D2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F469\\u200D\\U0001F9D2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F468\U0001F469\U0001F9D2]<- \"[\\U0001F468\\U0001F469\\U0001F9D2]\" using AddStr."); + top.Move (0, ++i); + top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F9D2]\" using AddStr."); + }; Application.Run (top); top.Dispose (); diff --git a/UnitTests/Application/ApplicationScreenTests.cs b/UnitTests/Application/ApplicationScreenTests.cs index f53bf8531b..ea965abce8 100644 --- a/UnitTests/Application/ApplicationScreenTests.cs +++ b/UnitTests/Application/ApplicationScreenTests.cs @@ -8,6 +8,7 @@ public class ApplicationScreenTests (ITestOutputHelper output) public void ClearScreenNextIteration_Resets_To_False_After_LayoutAndDraw () { // Arrange + ConsoleDriver.RunningUnitTests = true; Application.Init (); // Act diff --git a/UnitTests/ConsoleDrivers/AddRuneTests.cs b/UnitTests/ConsoleDrivers/AddRuneTests.cs index 5c753386ee..ed624b495c 100644 --- a/UnitTests/ConsoleDrivers/AddRuneTests.cs +++ b/UnitTests/ConsoleDrivers/AddRuneTests.cs @@ -55,7 +55,9 @@ public void AddRune_Accented_Letter_With_Three_Combining_Unicode_Chars () text = "\u0103\u0301"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Rune); + Assert.Single (driver.Contents [0, 0].CombiningMarks); + string combined = driver.Contents [0, 0].Rune + driver.Contents [0, 0].CombiningMarks [0].ToString (); + Assert.Equal (expected, (Rune)combined.Normalize (NormalizationForm.FormC) [0]); Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); driver.ClearContents (); @@ -63,7 +65,9 @@ public void AddRune_Accented_Letter_With_Three_Combining_Unicode_Chars () text = "\u0061\u0306\u0301"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Rune); + Assert.Equal (2, driver.Contents [0, 0].CombiningMarks.Count); + combined = driver.Contents [0, 0].Rune + driver.Contents [0, 0].CombiningMarks [0].ToString () + driver.Contents [0, 0].CombiningMarks [1]; + Assert.Equal (expected, (Rune)combined.Normalize (NormalizationForm.FormC) [0]); Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune); // var s = "a\u0301\u0300\u0306"; diff --git a/UnitTests/ConsoleDrivers/ContentsTests.cs b/UnitTests/ConsoleDrivers/ContentsTests.cs index d0f2d83ff7..6299c3a7f3 100644 --- a/UnitTests/ConsoleDrivers/ContentsTests.cs +++ b/UnitTests/ConsoleDrivers/ContentsTests.cs @@ -56,32 +56,36 @@ public void AddStr_With_Combining_Characters (Type driverType) // a + ogonek + acute = ( ą́ ) var ogonek = new Rune (0x0328); // Combining ogonek (a small hook or comma shape) combined = "a" + ogonek + acuteaccent; - expected = ("a" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("á" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + driver.ClearContents (); driver.Move (0, 0); driver.AddStr (combined); TestHelpers.AssertDriverContentsAre (expected, output, driver); // e + ogonek + acute = ( ę́́ ) combined = "e" + ogonek + acuteaccent; - expected = ("e" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("é" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + driver.ClearContents (); driver.Move (0, 0); driver.AddStr (combined); TestHelpers.AssertDriverContentsAre (expected, output, driver); // i + ogonek + acute = ( į́́́ ) combined = "i" + ogonek + acuteaccent; - expected = ("i" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("í" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + driver.ClearContents (); driver.Move (0, 0); driver.AddStr (combined); TestHelpers.AssertDriverContentsAre (expected, output, driver); // u + ogonek + acute = ( ų́́́́ ) combined = "u" + ogonek + acuteaccent; - expected = ("u" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + expected = ("ú" + ogonek).Normalize (NormalizationForm.FormC); // See Issue #2616 + driver.ClearContents (); driver.Move (0, 0); driver.AddStr (combined); TestHelpers.AssertDriverContentsAre (expected, output, driver); diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index ed687e61b0..fceb821584 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -409,7 +409,7 @@ public static Rectangle AssertDriverContentsWithFrameAre ( colIndex++; } - if (colIndex + 1 > w) + if (colIndex + 1 > w && !runeAtCurrentLocation.IsCombiningMark ()) { w = colIndex + 1; } @@ -417,15 +417,28 @@ public static Rectangle AssertDriverContentsWithFrameAre ( h = rowIndex - y + 1; } - if (x > -1) + if (x > -1 && contents [rowIndex, colIndex].CombiningMarks is null) { runes.Add (runeAtCurrentLocation); } - // See Issue #2616 - //foreach (var combMark in contents [r, c].CombiningMarks) { - // runes.Add (combMark); - //} + if (contents [rowIndex, colIndex].CombiningMarks is { Count: > 0 }) + { + string combine = runeAtCurrentLocation.ToString (); + string? normalized = null; + + // See Issue #2616 + foreach (var combMark in contents [rowIndex, colIndex].CombiningMarks) + { + combine += combMark; + normalized = combine.Normalize (NormalizationForm.FormC); + } + + foreach (Rune enumerateRune in normalized!.EnumerateRunes ()) + { + runes.Add (enumerateRune); + } + } } if (runes.Count > 0) diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 5435d02044..806ecc0502 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -4114,8 +4114,8 @@ public void Draw_Vertical_TopBottom_LeftRight_Top (string text, int height, stri } [Theory] - [InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misęrables")] - [InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę\nr\na\nb\nl\ne\ns")] + [InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misę́rables")] + [InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę́\nr\na\nb\nl\ne\ns")] [InlineData ( 4, 4, @@ -4124,7 +4124,7 @@ public void Draw_Vertical_TopBottom_LeftRight_Top (string text, int height, stri LMre eias ssb - ęl " + ę́l " )] public void Draw_With_Combining_Runes (int width, int height, TextDirection textDirection, string expected) { diff --git a/UnitTests/View/Draw/AllViewsDrawTests.cs b/UnitTests/View/Draw/AllViewsDrawTests.cs index fa865068bf..f9e6c806ac 100644 --- a/UnitTests/View/Draw/AllViewsDrawTests.cs +++ b/UnitTests/View/Draw/AllViewsDrawTests.cs @@ -51,5 +51,7 @@ public void AllViews_Draw_Does_Not_Layout (Type viewType) Assert.Equal (1, layoutStartedCount); Assert.Equal (1, layoutCompleteCount); } + + Application.ResetState (true); } }