diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 1a9ed928c..0c23a83ac 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -70,7 +70,9 @@ namespace Terminal.Gui { contents [crow, ccol - 1, 0] = (int)(uint)' '; Curses.move (crow, ccol); Curses.attrset (curAtttib); - } else if (runeWidth < 2 && ccol < Cols - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { + + } else if (runeWidth < 2 && ccol <= Clip.Right - 1 + && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { var curAtttib = currentAttribute; Curses.attrset (contents [crow, ccol + 1, 1]); @@ -78,9 +80,15 @@ namespace Terminal.Gui { contents [crow, ccol + 1, 0] = (int)(uint)' '; Curses.move (crow, ccol); Curses.attrset (curAtttib); + + } + if (runeWidth > 1 && ccol == Clip.Right - 1) { + Curses.addch ((int)(uint)' '); + contents [crow, ccol, 0] = (int)(uint)' '; + } else { + Curses.addch ((int)(uint)rune); + contents [crow, ccol, 0] = (int)(uint)rune; } - Curses.addch ((int)(uint)rune); - contents [crow, ccol, 0] = (int)(uint)rune; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; } else @@ -88,7 +96,7 @@ namespace Terminal.Gui { ccol++; if (runeWidth > 1) { - if (validClip) { + if (validClip && ccol < Clip.Right) { contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 0; } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index bb3900868..ffe45bc78 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -108,31 +108,34 @@ namespace Terminal.Gui { contents [crow, ccol - 1, 0] = (int)(uint)' '; - } else if (runeWidth < 2 && ccol < Cols - 1 + } else if (runeWidth < 2 && ccol <= Clip.Right - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { contents [crow, ccol + 1, 0] = (int)(uint)' '; - } + contents [crow, ccol + 1, 2] = 1; - contents [crow, ccol, 0] = (int)(uint)rune; + } + if (runeWidth > 1 && ccol == Clip.Right - 1) { + contents [crow, ccol, 0] = (int)(uint)' '; + } else { + contents [crow, ccol, 0] = (int)(uint)rune; + } contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; + dirtyLine [crow] = true; } else needMove = true; ccol++; if (runeWidth > 1) { - for (int i = 1; i < runeWidth; i++) { - if (validClip) { - contents [crow, ccol, 1] = currentAttribute; - contents [crow, ccol, 2] = 0; - } else { - break; - } - ccol++; + if (validClip && ccol < Clip.Right) { + contents [crow, ccol, 1] = currentAttribute; + contents [crow, ccol, 2] = 0; } + ccol++; } + //if (ccol == Cols) { // ccol = 0; // if (crow + 1 < Rows) @@ -248,17 +251,17 @@ namespace Terminal.Gui { var savedRow = FakeConsole.CursorTop; var savedCol = FakeConsole.CursorLeft; - for (int row = 0; row < rows; row++) { + var savedCursorVisible = FakeConsole.CursorVisible; + for (int row = top; row < rows; row++) { if (!dirtyLine [row]) continue; dirtyLine [row] = false; - for (int col = 0; col < cols; col++) { + for (int col = left; col < cols; col++) { FakeConsole.CursorTop = row; FakeConsole.CursorLeft = col; for (; col < cols; col++) { - if (col > 0 && contents [row, col, 2] == 0 - && Rune.ColumnWidth ((char)contents [row, col - 1, 0]) > 1) { - FakeConsole.CursorLeft = col + 1; + if (contents [row, col, 2] == 0) { + FakeConsole.CursorLeft++; continue; } @@ -273,6 +276,7 @@ namespace Terminal.Gui { } FakeConsole.CursorTop = savedRow; FakeConsole.CursorLeft = savedCol; + FakeConsole.CursorVisible = savedCursorVisible; } public override void Refresh () @@ -549,13 +553,13 @@ namespace Terminal.Gui { } Clip = new Rect (0, 0, Cols, Rows); - - contents = new int [Rows, Cols, 3]; - dirtyLine = new bool [Rows]; } public override void UpdateOffScreen () { + contents = new int [Rows, Cols, 3]; + dirtyLine = new bool [Rows]; + // Can raise an exception while is still resizing. try { for (int row = 0; row < rows; row++) { diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 9fba13876..39cdd42ed 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -114,7 +114,7 @@ namespace Terminal.Gui { int lastWindowHeight; int largestWindowHeight; #if PROCESS_REQUEST - bool neededProcessRequest; + bool neededProcessRequest; #endif public int NumberOfCSI { get; } @@ -140,7 +140,7 @@ namespace Terminal.Gui { inputReady.Reset (); } #if PROCESS_REQUEST - neededProcessRequest = false; + neededProcessRequest = false; #endif if (inputResultQueue.Count > 0) { return inputResultQueue.Dequeue (); @@ -205,10 +205,10 @@ namespace Terminal.Gui { return; } #if PROCESS_REQUEST - if (!neededProcessRequest) { - Console.Out.Write ("\x1b[6n"); - neededProcessRequest = true; - } + if (!neededProcessRequest) { + Console.Out.Write ("\x1b[6n"); + neededProcessRequest = true; + } #endif } } @@ -469,7 +469,7 @@ namespace Terminal.Gui { string value = ""; for (int i = 0; i < kChar.Length; i++) { var c = kChar [i]; - if (c == '[') { + if (c == '\u001b' || c == '[') { foundPoint++; } else if (foundPoint == 1 && c != ';' && c != '?') { value += c.ToString (); @@ -1242,12 +1242,18 @@ namespace Terminal.Gui { contents [crow, ccol - 1, 0] = (int)(uint)' '; - } else if (runeWidth < 2 && ccol < Cols - 1 + } else if (runeWidth < 2 && ccol <= Clip.Right - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { contents [crow, ccol + 1, 0] = (int)(uint)' '; + contents [crow, ccol + 1, 2] = 1; + + } + if (runeWidth > 1 && ccol == Clip.Right - 1) { + contents [crow, ccol, 0] = (int)(uint)' '; + } else { + contents [crow, ccol, 0] = (int)(uint)rune; } - contents [crow, ccol, 0] = (int)(uint)rune; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; @@ -1256,7 +1262,7 @@ namespace Terminal.Gui { ccol++; if (runeWidth > 1) { - if (validClip) { + if (validClip && ccol < Clip.Right) { contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 0; } @@ -1288,6 +1294,9 @@ namespace Terminal.Gui { StopReportingMouseMoves (); Console.ResetColor (); Clear (); + //Set cursor key to cursor. + Console.Out.Write ("\x1b[?25h"); + Console.Out.Flush (); } void Clear () @@ -1313,6 +1322,10 @@ namespace Terminal.Gui { { TerminalResized = terminalResized; + //Set cursor key to application. + Console.Out.Write ("\x1b[?25l"); + Console.Out.Flush (); + Console.TreatControlCAsInput = true; cols = Console.WindowWidth; @@ -1455,28 +1468,41 @@ namespace Terminal.Gui { } int top = Top; + int left = Left; int rows = Math.Min (Console.WindowHeight + top, Rows); int cols = Cols; + System.Text.StringBuilder output = new System.Text.StringBuilder (); + var lastCol = left; - var savedCursorVisible = Console.CursorVisible = false; + Console.CursorVisible = false; for (int row = top; row < rows; row++) { if (!dirtyLine [row]) { continue; } dirtyLine [row] = false; - System.Text.StringBuilder output = new System.Text.StringBuilder (); - for (int col = 0; col < cols; col++) { + output.Clear (); + for (int col = left; col < cols; col++) { if (Console.WindowHeight > 0 && !SetCursorPosition (col, row)) { return; } - var lastCol = -1; + lastCol = left; + var outputWidth = 0; for (; col < cols; col++) { - if (col > 0 && contents [row, col, 2] == 0 - && Rune.ColumnWidth ((char)contents [row, col - 1, 0]) > 1) { - - if (col == cols - 1 && output.Length > 0) { - Console.CursorLeft = lastCol; - Console.Write (output); + if (contents [row, col, 2] == 0) { + if (output.Length > 0) { + if (col > 0 && Rune.ColumnWidth ((char)contents [row, col - 1, 0]) < 2) { + Console.CursorLeft = lastCol; + Console.CursorTop = row; + Console.Write (output); + output.Clear (); + lastCol += outputWidth; + outputWidth = 0; + if (lastCol + 1 < cols) + lastCol++; + } + } else { + if (lastCol + 1 < cols) + lastCol++; } continue; } @@ -1484,27 +1510,27 @@ namespace Terminal.Gui { var attr = contents [row, col, 1]; if (attr != redrawAttr) { output.Append (WriteAttributes (attr)); - if (lastCol == -1) - lastCol = col; } if (AlwaysSetPosition && !SetCursorPosition (col, row)) { return; } + var rune = (char)contents [row, col, 0]; + outputWidth += Math.Max (Rune.ColumnWidth (rune), 1); if (AlwaysSetPosition) { - Console.Write ($"{output}{(char)contents [row, col, 0]}"); + Console.Write ($"{output}{rune}"); + output.Clear (); } else { - output.Append ((char)contents [row, col, 0]); - if (lastCol == -1) - lastCol = col; + output.Append (rune); } contents [row, col, 2] = 0; - if (!AlwaysSetPosition && col == cols - 1) { - Console.Write (output); - } } } + if (output.Length > 0) { + Console.CursorLeft = lastCol; + Console.CursorTop = row; + Console.Write (output); + } } - Console.CursorVisible = savedCursorVisible; } System.Text.StringBuilder WriteAttributes (int attr) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 4cea2421a..c0ea00e8a 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1480,15 +1480,22 @@ namespace Terminal.Gui { OutputBuffer [prevPosition].Char.UnicodeChar = ' '; contents [crow, ccol - 1, 0] = (int)(uint)' '; - } else if (runeWidth < 2 && ccol < Cols - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { + } else if (runeWidth < 2 && ccol <= Clip.Right - 1 + && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { var prevPosition = crow * Cols + ccol + 1; OutputBuffer [prevPosition].Char.UnicodeChar = (char)' '; contents [crow, ccol + 1, 0] = (int)(uint)' '; + + } + if (runeWidth > 1 && ccol == Clip.Right - 1) { + OutputBuffer [position].Char.UnicodeChar = (char)' '; + contents [crow, ccol, 0] = (int)(uint)' '; + } else { + OutputBuffer [position].Char.UnicodeChar = (char)rune; + contents [crow, ccol, 0] = (int)(uint)rune; } OutputBuffer [position].Attributes = (ushort)currentAttribute; - OutputBuffer [position].Char.UnicodeChar = (char)rune; - contents [crow, ccol, 0] = (int)(uint)rune; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; WindowsConsole.SmallRect.Update (ref damageRegion, (short)ccol, (short)crow); @@ -1496,7 +1503,7 @@ namespace Terminal.Gui { ccol++; if (runeWidth > 1) { - if (validClip) { + if (validClip && ccol < Clip.Right) { position = crow * Cols + ccol; OutputBuffer [position].Attributes = (ushort)currentAttribute; OutputBuffer [position].Char.UnicodeChar = (char)0x00; diff --git a/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs new file mode 100644 index 000000000..4d09b8c7e --- /dev/null +++ b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -0,0 +1,151 @@ +using System; +using System.Globalization; +using System.Text; +using System.Threading; +using Terminal.Gui; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "RuneWidthGreaterThanOne", Description: "Test rune width greater than one")] + [ScenarioCategory ("Controls")] + public class RuneWidthGreaterThanOne : Scenario { + private Label _label; + private TextField _text; + private Button _button; + private Label _labelR; + private Label _labelV; + private Window _win; + + public override void Init (Toplevel top, ColorScheme colorScheme) + { + Application.Init (); + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("Margin", new MenuItem [] { + new MenuItem ("With margin", "", WithMargin), + new MenuItem ("Without margin", "", WithoutMargin) + }), + new MenuBarItem ("Draw Margin Frame", new MenuItem [] { + new MenuItem ("With draw", "", WithDrawMargin), + new MenuItem ("Without draw", "", WithoutDrawMargin) + }), + new MenuBarItem ("Runes length", new MenuItem [] { + new MenuItem ("Wide", "", WideRunes), + new MenuItem ("Narrow", "", NarrowRunes), + new MenuItem ("Mixed", "", MixedRunes) + }) + }); + + _label = new Label (text: "あなたの名前を入力してください:") { + X = Pos.Center (), + Y = 0, + ColorScheme = new ColorScheme () { + Normal = Colors.Base.Focus + } + }; + _text = new TextField ("ティラミス") { + X = Pos.Center (), + Y = 2, + Width = 20 + }; + _button = new Button (text: "こんにちはと言う") { + X = Pos.Center (), + Y = 4 + }; + _button.Clicked += () => MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); + _labelR = new Label (text: "あなたの名前を入力してください") { + X = Pos.AnchorEnd (30), + Y = 18 + }; + _labelV = new Label (text: "あなたの名前を入力してください", TextDirection.TopBottom_RightLeft) { + X = Pos.AnchorEnd (30), + Y = Pos.Bottom (_labelR) + }; + _win = new Window ("デモエムポンズ") { + X = 5, + Y = 5, + Width = Dim.Fill (22), + Height = Dim.Fill (5) + }; + _win.Add (_label, _text, _button, _labelR, _labelV); + Application.Top.Add (menu, _win); + Application.Run (); + } + + private void MixedRunes () + { + _label.Text = "Enter your name 你:"; + _text.Text = "gui.cs 你:"; + _button.Text = "Say Hello 你"; + _button.Clicked += () => MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); + _labelR.X = Pos.AnchorEnd (21); + _labelR.Y = 18; + _labelR.Text = "This is a test text 你"; + _labelV.X = Pos.AnchorEnd (21); + _labelV.Y = Pos.Bottom (_labelR); + _labelV.Text = "This is a test text 你"; + _win.Title = "HACC Demo 你"; + } + + private void NarrowRunes () + { + _label.Text = "Enter your name:"; + _text.Text = "gui.cs"; + _button.Text = "Say Hello"; + _button.Clicked += () => MessageBox.Query ("Say Hello", $"Hello {_text.Text}", "Ok"); + _labelR.X = Pos.AnchorEnd (19); + _labelR.Y = 18; + _labelR.Text = "This is a test text"; + _labelV.X = Pos.AnchorEnd (19); + _labelV.Y = Pos.Bottom (_labelR); + _labelV.Text = "This is a test text"; + _win.Title = "HACC Demo"; + } + + private void WideRunes () + { + _label.Text = "あなたの名前を入力してください:"; + _text.Text = "ティラミス"; + _button.Text = "こんにちはと言う"; + _button.Clicked += () => MessageBox.Query ("こんにちはと言う", $"こんにちは {_text.Text}", "Ok"); + _labelR.X = Pos.AnchorEnd (29); + _labelR.Y = 18; + _labelR.Text = "あなたの名前を入力してください"; + _labelV.X = Pos.AnchorEnd (29); + _labelV.Y = Pos.Bottom (_labelR); + _labelV.Text = "あなたの名前を入力してください"; + _win.Title = "デモエムポンズ"; + } + + private void WithoutDrawMargin () + { + _win.Border.BorderStyle = BorderStyle.None; + _win.Border.DrawMarginFrame = false; + } + + private void WithDrawMargin () + { + _win.Border.DrawMarginFrame = true; + _win.Border.BorderStyle = BorderStyle.Single; + } + + private void WithoutMargin () + { + _win.X = 0; + _win.Y = 0; + _win.Width = Dim.Fill (); + _win.Height = Dim.Fill (); + } + + private void WithMargin () + { + _win.X = 5; + _win.Y = 5; + _win.Width = Dim.Fill (22); + _win.Height = Dim.Fill (5); + } + + public override void Run () + { + } + } +} \ No newline at end of file diff --git a/UnitTests/ConsoleDriverTests.cs b/UnitTests/ConsoleDriverTests.cs index 20f56fab1..5f85b2d10 100644 --- a/UnitTests/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDriverTests.cs @@ -1,14 +1,23 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Terminal.Gui; +using Terminal.Gui.Views; using Xunit; +using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; namespace Terminal.Gui.ConsoleDrivers { public class ConsoleDriverTests { + readonly ITestOutputHelper output; + + public ConsoleDriverTests (ITestOutputHelper output) + { + this.output = output; + } + [Fact] public void Init_Inits () { @@ -503,5 +512,101 @@ namespace Terminal.Gui.ConsoleDrivers { Assert.True (closed); Assert.Empty (FakeConsole.MockKeyPresses); } + + [Fact, AutoInitShutdown] + public void AddRune_On_Clip_Left_Or_Right_Replace_Previous_Or_Next_Wide_Rune_With_Space () + { + var tv = new TextView () { + Width = Dim.Fill (), + Height = Dim.Fill (), + Text = @"これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。 +これは広いルーンラインです。" + }; + var win = new Window ("ワイドルーン") { Width = Dim.Fill (), Height = Dim.Fill () }; + win.Add (tv); + Application.Top.Add (win); + var lbl = new Label ("ワイドルーン。"); + var dg = new Dialog ("テスト", 14, 4, new Button ("選ぶ")); + dg.Add (lbl); + Application.Begin (Application.Top); + Application.Begin (dg); + ((FakeDriver)Application.Driver).SetBufferSize (30, 10); + + var expected = @" +┌ ワイドルーン ──────────────┐ +│これは広いルーンラインです。│ +│これは広いルーンラインです。│ +│これは ┌ テスト ────┐ です。│ +│これは │ワイドルーン│ です。│ +│これは │ [ 選ぶ ] │ です。│ +│これは └────────────┘ です。│ +│これは広いルーンラインです。│ +│これは広いルーンラインです。│ +└────────────────────────────┘ +"; + + var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new Rect (0, 0, 30, 10), pos); + } + + [Fact, AutoInitShutdown] + public void Write_Do_Not_Change_On_ProcessKey () + { + var win = new Window (); + Application.Begin (win); + ((FakeDriver)Application.Driver).SetBufferSize (20, 8); + + System.Threading.Tasks.Task.Run (() => { + System.Threading.Tasks.Task.Delay (500).Wait (); + Application.MainLoop.Invoke (() => { + var lbl = new Label ("Hello World") { X = Pos.Center () }; + var dlg = new Dialog ("Test", new Button ("Ok")); + dlg.Add (lbl); + Application.Begin (dlg); + + var expected = @" +┌──────────────────┐ +│┌ Test ─────────┐ │ +││ Hello World │ │ +││ │ │ +││ │ │ +││ [ Ok ] │ │ +│└───────────────┘ │ +└──────────────────┘ +"; + + var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new Rect (0, 0, 20, 8), pos); + + Assert.True (dlg.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + dlg.Redraw (dlg.Bounds); + + expected = @" +┌──────────────────┐ +│┌ Test ─────────┐ │ +││ Hello World │ │ +││ │ │ +││ │ │ +││ [ Ok ] │ │ +│└───────────────┘ │ +└──────────────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new Rect (0, 0, 20, 8), pos); + + win.RequestStop (); + }); + }); + + Application.Run (win); + Application.Shutdown (); + } } } diff --git a/UnitTests/GraphViewTests.cs b/UnitTests/GraphViewTests.cs index 161dc1034..513380b98 100644 --- a/UnitTests/GraphViewTests.cs +++ b/UnitTests/GraphViewTests.cs @@ -159,8 +159,10 @@ namespace Terminal.Gui.Views { } // Remove unnecessary empty lines - for (int r = lines.Count - 1; r > h - 1; r--) { - lines.RemoveAt (r); + if (lines.Count > 0) { + for (int r = lines.Count - 1; r > h - 1; r--) { + lines.RemoveAt (r); + } } // Remove trailing whitespace on each line