diff --git a/Terminal.Gui/Clipboard/ClipboardBase.cs b/Terminal.Gui/Clipboard/ClipboardBase.cs
index 8c1c0a93a..2406cfe47 100644
--- a/Terminal.Gui/Clipboard/ClipboardBase.cs
+++ b/Terminal.Gui/Clipboard/ClipboardBase.cs
@@ -22,7 +22,7 @@ public abstract class ClipboardBase : IClipboard
return string.Empty;
}
- return GetClipboardDataImpl ();
+ return result;
}
catch (NotSupportedException ex)
{
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index 8d418dddb..fb5136316 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -691,6 +691,40 @@ public abstract class ConsoleDriver : IConsoleDriver
/// If simulates the Ctrl key being pressed.
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
+ internal char _highSurrogate = '\0';
+
+ internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
+ {
+ result = keyCode;
+
+ if (char.IsHighSurrogate ((char)keyCode))
+ {
+ _highSurrogate = (char)keyCode;
+
+ return false;
+ }
+
+ if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
+ {
+ result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
+ _highSurrogate = '\0';
+
+ return true;
+ }
+
+ if (char.IsSurrogate ((char)keyCode))
+ {
+ return false;
+ }
+
+ if (_highSurrogate > 0)
+ {
+ _highSurrogate = '\0';
+ }
+
+ return true;
+ }
+
#endregion
private AnsiRequestScheduler? _scheduler;
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs
index 0ec8efc05..85f9cd88c 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs
@@ -203,7 +203,7 @@ internal class WSLClipboard : ClipboardBase
}
(int exitCode, string output) =
- ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"Get-Clipboard\"");
+ ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard\"");
if (exitCode == 0)
{
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 48bcb713f..f8101d546 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -924,8 +924,11 @@ internal class CursesDriver : ConsoleDriver
k &= ~KeyCode.Space;
}
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
+ if (IsValidInput (k, out k))
+ {
+ OnKeyDown (new (k));
+ OnKeyUp (new (k));
+ }
}
}
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
index 5e9a883d7..3dc008242 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
@@ -352,8 +352,12 @@ public class FakeDriver : ConsoleDriver
}
KeyCode map = MapKey (consoleKeyInfo);
- OnKeyDown (new Key (map));
- OnKeyUp (new Key (map));
+
+ if (IsValidInput (map, out map))
+ {
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
//OnKeyPressed (new KeyEventArgs (map));
}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
index 2ede00a00..59d75b581 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
@@ -321,8 +321,11 @@ internal class NetDriver : ConsoleDriver
break;
}
- OnKeyDown (new (map));
- OnKeyUp (new (map));
+ if (IsValidInput (map, out map))
+ {
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
break;
case EventType.Mouse:
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
index 1f74c0321..1fc1eb842 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
@@ -507,9 +507,12 @@ internal class WindowsDriver : ConsoleDriver
break;
}
- // This follows convention in NetDriver
- OnKeyDown (new Key (map));
- OnKeyUp (new Key (map));
+ if (IsValidInput (map, out map))
+ {
+ // This follows convention in NetDriver
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
break;
diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs
index e0d4119aa..dfcd34e67 100644
--- a/Terminal.Gui/Input/Keyboard/Key.cs
+++ b/Terminal.Gui/Input/Keyboard/Key.cs
@@ -138,6 +138,44 @@ public class Key : EventArgs, IEquatable
KeyCode = key.KeyCode;
}
+ ///
+ /// Constructs a new Key from an integer describing the key.
+ /// It parses the integer as Key by calling the constructor with a char or calls the constructor with a
+ /// KeyCode.
+ ///
+ ///
+ /// Don't rely on passed from to because
+ /// would not return the expected keys from 'a' to 'z'.
+ ///
+ /// The integer describing the key.
+ ///
+ ///
+ public Key (int value)
+ {
+ if (value < 0 || value > RuneExtensions.MaxUnicodeCodePoint)
+ {
+ throw new ArgumentOutOfRangeException (@$"Invalid key value: {value}", nameof (value));
+ }
+
+ if (char.IsSurrogate ((char)value))
+ {
+ throw new ArgumentException (@$"Surrogate key not allowed: {value}", nameof (value));
+ }
+
+ Key key;
+
+ if (((Rune)value).IsBmp)
+ {
+ key = new ((char)value);
+ }
+ else
+ {
+ key = new ((KeyCode)value);
+ }
+
+ KeyCode = key.KeyCode;
+ }
+
///
/// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers.
/// Useful for determining if a key represents is a printable character.
@@ -388,6 +426,11 @@ public class Key : EventArgs, IEquatable
///
public static implicit operator Key (string str) { return new (str); }
+ /// Cast to a .
+ /// See for more information.
+ ///
+ public static implicit operator Key (int value) { return new (value); }
+
/// Cast a to a .
/// See for more information.
///
@@ -550,7 +593,7 @@ public class Key : EventArgs, IEquatable
// "Ctrl+" (trim)
// "Ctrl++" (trim)
- if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
+ if (input.Length > 1 && !char.IsHighSurrogate (input [^2]) && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
{
return input [..^1];
}
@@ -640,6 +683,13 @@ public class Key : EventArgs, IEquatable
return false;
}
+ if (text.Length == 2 && char.IsHighSurrogate (text [^2]) && char.IsLowSurrogate (text [^1]))
+ {
+ // It's a surrogate pair and there is no modifiers
+ key = new (new Rune (text [^2], text [^1]).Value);
+ return true;
+ }
+
// e.g. "Ctrl++"
if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty))
{
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index a6e5d58c1..07adca219 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -729,21 +729,11 @@ public class TextField : View
/// Use the previous cursor position.
public void InsertText (string toAdd, bool useOldCursorPos = true)
{
- foreach (char ch in toAdd)
+ foreach (Rune rune in toAdd.EnumerateRunes ())
{
- Key key;
-
- try
- {
- key = ch;
- }
- catch (Exception)
- {
- throw new ArgumentException (
- $"Cannot insert character '{ch}' because it does not map to a Key"
- );
- }
-
+ // All rune can be mapped to a Key and no exception will throw here because
+ // EnumerateRunes will replace a surrogate char with the Rune.ReplacementChar
+ Key key = rune.Value;
InsertText (key, useOldCursorPos);
}
}
@@ -1072,14 +1062,20 @@ public class TextField : View
/// Paste the selected text from the clipboard.
public virtual void Paste ()
{
- if (ReadOnly || string.IsNullOrEmpty (Clipboard.Contents))
+ if (ReadOnly)
+ {
+ return;
+ }
+
+ string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? "";
+
+ if (string.IsNullOrEmpty (cbTxt))
{
return;
}
SetSelectedStartSelectedLength ();
int selStart = _start == -1 ? CursorPosition : _start;
- string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? "";
Text = StringExtensions.ToString (_text.GetRange (0, selStart))
+ cbTxt
@@ -1114,7 +1110,7 @@ public class TextField : View
TextModel.SetCol (ref col, Viewport.Width - 1, cols);
}
- int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0);
+ int pos = col - ScrollOffset + Math.Min (Viewport.X, 0);
Move (pos, 0);
return new Point (pos, 0);
diff --git a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj
deleted file mode 100644
index 03c8b09d9..000000000
--- a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net8.0
- enable
- enable
-
-
-
-
-
-
-
-
diff --git a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs
deleted file mode 100644
index 53f81e37b..000000000
--- a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Xunit;
-
-namespace TerminalGuiFluentTesting;
-
-public static class XunitContextExtensions
-{
- public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition)
- {
- context.Then (
- () =>
- {
- Assert.True (condition);
- });
- return context;
- }
- public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual)
- {
- context.Then (
- () =>
- {
- Assert.Equal (expected,actual);
- });
- return context;
- }
-}
diff --git a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj
index e9e661df2..2b556191b 100644
--- a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj
+++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj
@@ -11,7 +11,9 @@
+
+
diff --git a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
index fbede01b0..de2965278 100644
--- a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
+++ b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
@@ -283,4 +283,115 @@ public class ConsoleDriverTests
// Application.Run (win);
// Application.Shutdown ();
// }
+
+ [Theory]
+ [InlineData ('\ud83d', '\udcc4')] // This seems right sequence but Stack is LIFO
+ [InlineData ('\ud83d', '\ud83d')]
+ [InlineData ('\udcc4', '\udcc4')]
+ public void FakeDriver_IsValidInput_Wrong_Surrogate_Sequence (char c1, char c2)
+ {
+ var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+ Application.Init (driver);
+
+ Stack mKeys = new (
+ [
+ new ('a', ConsoleKey.A, false, false, false),
+ new (c1, ConsoleKey.None, false, false, false),
+ new (c2, ConsoleKey.None, false, false, false)
+ ]);
+
+ Console.MockKeyPresses = mKeys;
+
+ Toplevel top = new ();
+ var view = new View { CanFocus = true };
+ var rText = "";
+ var idx = 0;
+
+ view.KeyDown += (s, e) =>
+ {
+ Assert.Equal (new ('a'), e.AsRune);
+ Assert.Equal ("a", e.AsRune.ToString ());
+ rText += e.AsRune;
+ e.Handled = true;
+ idx++;
+ };
+ top.Add (view);
+
+ Application.Iteration += (s, a) =>
+ {
+ if (mKeys.Count == 0)
+ {
+ Application.RequestStop ();
+ }
+ };
+
+ Application.Run (top);
+
+ Assert.Equal ("a", rText);
+ Assert.Equal (1, idx);
+ Assert.Equal (0, ((FakeDriver)driver)._highSurrogate);
+
+ top.Dispose ();
+
+ // Shutdown must be called to safely clean up Application if Init has been called
+ Application.Shutdown ();
+ }
+
+ [Fact]
+ public void FakeDriver_IsValidInput_Correct_Surrogate_Sequence ()
+ {
+ var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+ Application.Init (driver);
+
+ Stack mKeys = new (
+ [
+ new ('a', ConsoleKey.A, false, false, false),
+ new ('\udcc4', ConsoleKey.None, false, false, false),
+ new ('\ud83d', ConsoleKey.None, false, false, false)
+ ]);
+
+ Console.MockKeyPresses = mKeys;
+
+ Toplevel top = new ();
+ var view = new View { CanFocus = true };
+ var rText = "";
+ var idx = 0;
+
+ view.KeyDown += (s, e) =>
+ {
+ if (idx == 0)
+ {
+ Assert.Equal (new (0x1F4C4), e.AsRune);
+ Assert.Equal ("📄", e.AsRune.ToString ());
+ }
+ else
+ {
+ Assert.Equal (new ('a'), e.AsRune);
+ Assert.Equal ("a", e.AsRune.ToString ());
+ }
+
+ rText += e.AsRune;
+ e.Handled = true;
+ idx++;
+ };
+ top.Add (view);
+
+ Application.Iteration += (s, a) =>
+ {
+ if (mKeys.Count == 0)
+ {
+ Application.RequestStop ();
+ }
+ };
+
+ Application.Run (top);
+
+ Assert.Equal ("📄a", rText);
+ Assert.Equal (2, idx);
+
+ top.Dispose ();
+
+ // Shutdown must be called to safely clean up Application if Init has been called
+ Application.Shutdown ();
+ }
}
diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs
index 76e970706..fd69ea289 100644
--- a/Tests/UnitTests/Views/TextFieldTests.cs
+++ b/Tests/UnitTests/Views/TextFieldTests.cs
@@ -78,27 +78,6 @@ public class TextFieldTests (ITestOutputHelper output)
}
}
- [Fact]
- public void Cancel_TextChanging_ThenBackspace ()
- {
- var tf = new TextField ();
- tf.SetFocus ();
- tf.NewKeyDownEvent (Key.A.WithShift);
- Assert.Equal ("A", tf.Text);
-
- // cancel the next keystroke
- tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB";
- tf.NewKeyDownEvent (Key.B.WithShift);
-
- // B was canceled so should just be A
- Assert.Equal ("A", tf.Text);
-
- // now delete the A
- tf.NewKeyDownEvent (Key.Backspace);
-
- Assert.Equal ("", tf.Text);
- }
-
[Fact]
[TextFieldTestsAutoInitShutdown]
public void CanFocus_False_Wont_Focus_With_Mouse ()
@@ -506,77 +485,6 @@ public class TextFieldTests (ITestOutputHelper output)
top.Dispose ();
}
- [Fact]
- public void HistoryText_IsDirty_ClearHistoryChanges ()
- {
- var text = "Testing";
- var tf = new TextField { Text = text };
- tf.BeginInit ();
- tf.EndInit ();
-
- Assert.Equal (text, tf.Text);
- tf.ClearHistoryChanges ();
- Assert.False (tf.IsDirty);
-
- Assert.True (tf.NewKeyDownEvent (Key.A.WithShift));
- Assert.Equal ($"{text}A", tf.Text);
- Assert.True (tf.IsDirty);
- }
-
- [Fact]
- public void Space_Does_Not_Raise_Selected ()
- {
- TextField tf = new ();
-
- tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied.");
-
- Application.Top = new ();
- Application.Top.Add (tf);
- tf.SetFocus ();
- Application.RaiseKeyDownEvent (Key.Space);
-
- Application.Top.Dispose ();
- Application.ResetState (true);
- }
-
- [Fact]
- public void Enter_Does_Not_Raise_Selected ()
- {
- TextField tf = new ();
-
- var selectingCount = 0;
- tf.Selecting += (sender, args) => selectingCount++;
-
- Application.Top = new ();
- Application.Top.Add (tf);
- tf.SetFocus ();
- Application.RaiseKeyDownEvent (Key.Enter);
-
- Assert.Equal (0, selectingCount);
-
- Application.Top.Dispose ();
- Application.ResetState (true);
- }
-
- [Fact]
- public void Enter_Raises_Accepted ()
- {
- TextField tf = new ();
-
- var acceptedCount = 0;
- tf.Accepting += (sender, args) => acceptedCount++;
-
- Application.Top = new ();
- Application.Top.Add (tf);
- tf.SetFocus ();
- Application.RaiseKeyDownEvent (Key.Enter);
-
- Assert.Equal (1, acceptedCount);
-
- Application.Top.Dispose ();
- Application.ResetState (true);
- }
-
[Fact]
[AutoInitShutdown (useFakeClipboard: true)]
public void KeyBindings_Command ()
@@ -811,47 +719,6 @@ public class TextFieldTests (ITestOutputHelper output)
Assert.Equal ("", tf.Text);
}
- [Fact]
- public void HotKey_Command_SetsFocus ()
- {
- var view = new TextField ();
-
- view.CanFocus = true;
- Assert.False (view.HasFocus);
- view.InvokeCommand (Command.HotKey);
- Assert.True (view.HasFocus);
- }
-
- [Fact]
- public void HotKey_Command_Does_Not_Accept ()
- {
- var view = new TextField ();
- var accepted = false;
- view.Accepting += OnAccept;
- view.InvokeCommand (Command.HotKey);
-
- Assert.False (accepted);
-
- return;
-
- void OnAccept (object sender, CommandEventArgs e) { accepted = true; }
- }
-
- [Fact]
- public void Accepted_Command_Fires_Accept ()
- {
- var view = new TextField ();
-
- var accepted = false;
- view.Accepting += Accept;
- view.InvokeCommand (Command.Accept);
- Assert.True (accepted);
-
- return;
-
- void Accept (object sender, CommandEventArgs e) { accepted = true; }
- }
-
[Theory]
[InlineData (false, 1)]
[InlineData (true, 0)]
@@ -904,87 +771,6 @@ public class TextFieldTests (ITestOutputHelper output)
void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
}
- [Fact]
- public void Accepted_No_Handler_Enables_Default_Button_Accept ()
- {
- var superView = new Window
- {
- Id = "superView"
- };
-
- var tf = new TextField
- {
- Id = "tf"
- };
-
- var button = new Button
- {
- Id = "button",
- IsDefault = true
- };
-
- superView.Add (tf, button);
-
- var buttonAccept = 0;
- button.Accepting += ButtonAccept;
-
- tf.SetFocus ();
- Assert.True (tf.HasFocus);
-
- superView.NewKeyDownEvent (Key.Enter);
- Assert.Equal (1, buttonAccept);
-
- button.SetFocus ();
- superView.NewKeyDownEvent (Key.Enter);
- Assert.Equal (2, buttonAccept);
-
- return;
-
- void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
- }
-
- [Fact]
- public void Accepted_Cancel_Event_HandlesCommand ()
- {
- //var super = new View ();
- var view = new TextField ();
-
- //super.Add (view);
-
- //var superAcceptedInvoked = false;
-
- var tfAcceptedInvoked = false;
- var handle = false;
- view.Accepting += TextViewAccept;
- Assert.False (view.InvokeCommand (Command.Accept));
- Assert.True (tfAcceptedInvoked);
-
- tfAcceptedInvoked = false;
- handle = true;
- view.Accepting += TextViewAccept;
- Assert.True (view.InvokeCommand (Command.Accept));
- Assert.True (tfAcceptedInvoked);
-
- return;
-
- void TextViewAccept (object sender, CommandEventArgs e)
- {
- tfAcceptedInvoked = true;
- e.Cancel = handle;
- }
- }
-
- [Fact]
- public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility ()
- {
- var top = new Toplevel ();
- var tf = new TextField { Width = 10 };
- top.Add (tf);
-
- Exception exception = Record.Exception (() => tf.SetFocus ());
- Assert.Null (exception);
- }
-
[Fact]
[TextFieldTestsAutoInitShutdown]
public void Paste_Always_Clear_The_SelectedText ()
@@ -997,59 +783,6 @@ public class TextFieldTests (ITestOutputHelper output)
Assert.Null (_textField.SelectedText);
}
- [Fact]
- public void Backspace_From_End ()
- {
- var tf = new TextField { Text = "ABC" };
- tf.SetFocus ();
- Assert.Equal ("ABC", tf.Text);
- tf.BeginInit ();
- tf.EndInit ();
-
- Assert.Equal (3, tf.CursorPosition);
-
- // now delete the C
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("AB", tf.Text);
- Assert.Equal (2, tf.CursorPosition);
-
- // then delete the B
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("A", tf.Text);
- Assert.Equal (1, tf.CursorPosition);
-
- // then delete the A
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("", tf.Text);
- Assert.Equal (0, tf.CursorPosition);
- }
-
- [Fact]
- public void Backspace_From_Middle ()
- {
- var tf = new TextField { Text = "ABC" };
- tf.SetFocus ();
- tf.CursorPosition = 2;
- Assert.Equal ("ABC", tf.Text);
-
- // now delete the B
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("AC", tf.Text);
-
- // then delete the A
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("C", tf.Text);
-
- // then delete nothing
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("C", tf.Text);
-
- // now delete the C
- tf.CursorPosition = 1;
- tf.NewKeyDownEvent (Key.Backspace);
- Assert.Equal ("", tf.Text);
- }
-
[Fact]
[AutoInitShutdown]
public void ScrollOffset_Initialize ()
@@ -1142,36 +875,6 @@ public class TextFieldTests (ITestOutputHelper output)
Assert.Null (_textField.SelectedText);
}
- [Fact]
- public void KeyDown_Handled_Prevents_Input ()
- {
- var tf = new TextField ();
- tf.KeyDown += HandleJKey;
-
- tf.NewKeyDownEvent (Key.A);
- Assert.Equal ("a", tf.Text);
-
- // SuppressKey suppresses the 'j' key
- tf.NewKeyDownEvent (Key.J);
- Assert.Equal ("a", tf.Text);
-
- tf.KeyDown -= HandleJKey;
-
- // Now that the delegate has been removed we can type j again
- tf.NewKeyDownEvent (Key.J);
- Assert.Equal ("aj", tf.Text);
-
- return;
-
- void HandleJKey (object s, Key arg)
- {
- if (arg.AsRune == new Rune ('j'))
- {
- arg.Handled = true;
- }
- }
- }
-
[Fact]
[AutoInitShutdown]
public void MouseEvent_Handled_Prevents_RightClick ()
@@ -1222,33 +925,6 @@ public class TextFieldTests (ITestOutputHelper output)
}
}
- [InlineData ("a")] // Lower than selection
- [InlineData ("aaaaaaaaaaa")] // Greater than selection
- [InlineData ("aaaa")] // Equal than selection
- [Theory]
- public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText)
- {
- var tf = new TextField ();
- tf.Text = "fish";
- tf.CursorPosition = tf.Text.Length;
-
- tf.NewKeyDownEvent (Key.CursorLeft);
-
- tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
- tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
-
- Assert.Equal (1, tf.CursorPosition);
- Assert.Equal (2, tf.SelectedLength);
- Assert.Equal ("is", tf.SelectedText);
-
- tf.Text = newText;
- tf.CursorPosition = tf.Text.Length;
-
- Assert.Equal (newText.Length, tf.CursorPosition);
- Assert.Equal (0, tf.SelectedLength);
- Assert.Null (tf.SelectedText);
- }
-
[Fact]
[TextFieldTestsAutoInitShutdown]
public void Text_Replaces_Tabs_With_Empty_String ()
@@ -1296,22 +972,6 @@ public class TextFieldTests (ITestOutputHelper output)
Assert.Equal ("changing", _textField.Text);
}
- [Fact]
- public void SpaceHandling ()
- {
- var tf = new TextField { Width = 10, Text = " " };
-
- var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked };
-
- tf.NewMouseEvent (ev);
- Assert.Equal (1, tf.SelectedLength);
-
- ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked };
-
- tf.NewMouseEvent (ev);
- Assert.Equal (1, tf.SelectedLength);
- }
-
[Fact]
[TextFieldTestsAutoInitShutdown]
public void Used_Is_False ()
@@ -1631,77 +1291,6 @@ public class TextFieldTests (ITestOutputHelper output)
}
}
- [Fact]
- public void WordBackward_WordForward_Mixed ()
- {
- var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" };
- tf.BeginInit ();
- tf.EndInit ();
-
- tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
- Assert.Equal (15, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
- Assert.Equal (12, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
- Assert.Equal (10, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
- Assert.Equal (5, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
- Assert.Equal (0, tf.CursorPosition);
-
- tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
- Assert.Equal (5, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
- Assert.Equal (10, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
- Assert.Equal (12, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
- Assert.Equal (15, tf.CursorPosition);
- tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
- Assert.Equal (22, tf.CursorPosition);
- }
-
- [Fact]
- public void WordBackward_WordForward_SelectedText_With_Accent ()
- {
- var text = "Les Misérables movie.";
- var tf = new TextField { Width = 30, Text = text };
-
- Assert.Equal (21, text.Length);
- Assert.Equal (21, tf.Text.GetRuneCount ());
- Assert.Equal (21, tf.Text.GetColumns ());
-
- List runes = tf.Text.ToRuneList ();
- Assert.Equal (21, runes.Count);
- Assert.Equal (21, tf.Text.Length);
-
- for (var i = 0; i < runes.Count; i++)
- {
- char cs = text [i];
- var cus = (char)runes [i].Value;
- Assert.Equal (cs, cus);
- }
-
- var idx = 15;
- Assert.Equal ('m', text [idx]);
- Assert.Equal ('m', (char)runes [idx].Value);
- Assert.Equal ("m", runes [idx].ToString ());
-
- Assert.True (
- tf.NewMouseEvent (
- new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
- )
- );
- Assert.Equal ("movie.", tf.SelectedText);
-
- Assert.True (
- tf.NewMouseEvent (
- new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
- )
- );
- Assert.Equal ("movie.", tf.SelectedText);
- }
-
[Fact]
[TextFieldTestsAutoInitShutdown]
public void WordForward_With_No_Selection ()
@@ -2056,115 +1645,6 @@ Les Miśerables",
}
}
- [Fact]
- public void Autocomplete_Popup_Added_To_SuperView_On_Init ()
- {
- View superView = new ()
- {
- CanFocus = true
- };
-
- TextField t = new ();
-
- superView.Add (t);
- Assert.Single (superView.SubViews);
-
- superView.BeginInit ();
- superView.EndInit ();
-
- Assert.Equal (2, superView.SubViews.Count);
- }
-
- [Fact]
- public void Autocomplete__Added_To_SuperView_On_Add ()
- {
- View superView = new ()
- {
- CanFocus = true,
- Id = "superView"
- };
-
- superView.BeginInit ();
- superView.EndInit ();
- Assert.Empty (superView.SubViews);
-
- TextField t = new ()
- {
- Id = "t"
- };
-
- superView.Add (t);
-
- Assert.Equal (2, superView.SubViews.Count);
- }
-
- [Fact]
- public void Right_CursorAtEnd_WithSelection_ShouldClearSelection ()
- {
- var tf = new TextField
- {
- Text = "Hello",
- };
- tf.SetFocus ();
- tf.SelectAll ();
- tf.CursorPosition = 5;
-
- // When there is selected text and the cursor is at the end of the text field
- Assert.Equal ("Hello", tf.SelectedText);
-
- // Pressing right should not move focus, instead it should clear selection
- Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
- Assert.Null (tf.SelectedText);
-
- // Now that the selection is cleared another right keypress should move focus
- Assert.False (tf.NewKeyDownEvent (Key.CursorRight));
- }
- [Fact]
- public void Left_CursorAtStart_WithSelection_ShouldClearSelection ()
- {
- var tf = new TextField
- {
- Text = "Hello",
- };
- tf.SetFocus ();
-
- tf.CursorPosition = 2;
- Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
- Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
-
- // When there is selected text and the cursor is at the start of the text field
- Assert.Equal ("He", tf.SelectedText);
-
- // Pressing left should not move focus, instead it should clear selection
- Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
- Assert.Null (tf.SelectedText);
-
- // When clearing selected text with left the cursor should be at the start of the selection
- Assert.Equal (0, tf.CursorPosition);
-
- // Now that the selection is cleared another left keypress should move focus
- Assert.False (tf.NewKeyDownEvent (Key.CursorLeft));
- }
- [Fact]
- public void Autocomplete_Visible_False_By_Default ()
- {
- View superView = new ()
- {
- CanFocus = true
- };
-
- TextField t = new ();
-
- superView.Add (t);
- superView.BeginInit ();
- superView.EndInit ();
-
- Assert.Equal (2, superView.SubViews.Count);
-
- Assert.True (t.Visible);
- Assert.False (t.Autocomplete.Visible);
- }
-
[Fact]
[AutoInitShutdown]
public void Draw_Esc_Rune ()
diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
index 0fcb4bead..26605314a 100644
--- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
+++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
@@ -52,7 +52,8 @@ public class KeyTests
{ "Ctrl-A", Key.A.WithCtrl },
{ "Alt-A", Key.A.WithAlt },
{ "A-Ctrl", Key.A.WithCtrl },
- { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt }
+ { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt },
+ { "📄", (KeyCode)0x1F4C4 }
};
[Theory]
@@ -120,10 +121,13 @@ public class KeyTests
[InlineData ('\'', (KeyCode)'\'')]
[InlineData ('\xFFFF', (KeyCode)0xFFFF)]
[InlineData ('\x0', (KeyCode)0x0)]
- public void Cast_Char_To_Key (char ch, KeyCode expectedKeyCode)
+ public void Cast_Char_Int_To_Key (char ch, KeyCode expectedKeyCode)
{
var key = (Key)ch;
Assert.Equal (expectedKeyCode, key.KeyCode);
+
+ key = (int)ch;
+ Assert.Equal (expectedKeyCode, key.KeyCode);
}
[Fact]
@@ -140,23 +144,25 @@ public class KeyTests
[InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)]
[InlineData (KeyCode.Z, KeyCode.Z)]
[InlineData (KeyCode.Space, KeyCode.Space)]
- public void Cast_KeyCode_To_Key (KeyCode cdk, KeyCode expected)
+ public void Cast_KeyCode_Int_To_Key (KeyCode cdk, KeyCode expected)
{
- // explicit
+ // KeyCode
var key = (Key)cdk;
Assert.Equal (((Key)expected).ToString (), key.ToString ());
- // implicit
- key = cdk;
+ // Int
+ key = key.AsRune.Value;
Assert.Equal (((Key)expected).ToString (), key.ToString ());
}
// string cast operators
- [Fact]
- public void Cast_String_To_Key ()
+ [Theory]
+ [InlineData ("Ctrl+Q", KeyCode.Q | KeyCode.CtrlMask)]
+ [InlineData ("📄", (KeyCode)0x1F4C4)]
+ public void Cast_String_To_Key (string str, KeyCode expectedKeyCode)
{
- var key = (Key)"Ctrl+Q";
- Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, key.KeyCode);
+ var key = (Key)str;
+ Assert.Equal (expectedKeyCode, key.KeyCode);
}
[Theory]
@@ -190,12 +196,37 @@ public class KeyTests
[InlineData ('\'', (KeyCode)'\'')]
[InlineData ('\xFFFF', (KeyCode)0xFFFF)]
[InlineData ('\x0', (KeyCode)0x0)]
- public void Constructor_Char (char ch, KeyCode expectedKeyCode)
+ public void Constructor_Char_Int (char ch, KeyCode expectedKeyCode)
{
var key = new Key (ch);
Assert.Equal (expectedKeyCode, key.KeyCode);
+
+ key = new ((int)ch);
+ Assert.Equal (expectedKeyCode, key.KeyCode);
}
+ [Theory]
+ [InlineData (0x1F4C4, (KeyCode)0x1F4C4, "📄")]
+ [InlineData (0x1F64B, (KeyCode)0x1F64B, "🙋")]
+ [InlineData (0x1F46A, (KeyCode)0x1F46A, "👪")]
+ public void Constructor_Int_Non_Bmp (int value, KeyCode expectedKeyCode, string expectedString)
+ {
+ var key = new Key (value);
+ Assert.Equal (expectedKeyCode, key.KeyCode);
+ Assert.Equal (expectedString, key.AsRune.ToString ());
+ Assert.Equal (expectedString, key.ToString ());
+ }
+
+ [Theory]
+ [InlineData (-1)]
+ [InlineData (0x11FFFF)]
+ public void Constructor_Int_Invalid_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); }
+
+ [Theory]
+ [InlineData ('\ud83d')]
+ [InlineData ('\udcc4')]
+ public void Constructor_Int_Surrogate_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); }
+
[Fact]
public void Constructor_Default_ShouldSetKeyToNull ()
{
diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs
new file mode 100644
index 000000000..d9019dfff
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs
@@ -0,0 +1,554 @@
+using System.Text;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class TextFieldTests
+{
+ [Fact]
+ public void Cancel_TextChanging_ThenBackspace ()
+ {
+ var tf = new TextField ();
+ tf.SetFocus ();
+ tf.NewKeyDownEvent (Key.A.WithShift);
+ Assert.Equal ("A", tf.Text);
+
+ // cancel the next keystroke
+ tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB";
+ tf.NewKeyDownEvent (Key.B.WithShift);
+
+ // B was canceled so should just be A
+ Assert.Equal ("A", tf.Text);
+
+ // now delete the A
+ tf.NewKeyDownEvent (Key.Backspace);
+
+ Assert.Equal ("", tf.Text);
+ }
+
+ [Fact]
+ public void HistoryText_IsDirty_ClearHistoryChanges ()
+ {
+ var text = "Testing";
+ var tf = new TextField { Text = text };
+ tf.BeginInit ();
+ tf.EndInit ();
+
+ Assert.Equal (text, tf.Text);
+ tf.ClearHistoryChanges ();
+ Assert.False (tf.IsDirty);
+
+ Assert.True (tf.NewKeyDownEvent (Key.A.WithShift));
+ Assert.Equal ($"{text}A", tf.Text);
+ Assert.True (tf.IsDirty);
+ }
+
+ [Fact]
+ public void Space_Does_Not_Raise_Selected ()
+ {
+ TextField tf = new ();
+
+ tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied.");
+
+ Toplevel top = new ();
+ top.Add (tf);
+ tf.SetFocus ();
+ top.NewKeyDownEvent (Key.Space);
+
+ top.Dispose ();
+ }
+
+ [Fact]
+ public void Enter_Does_Not_Raise_Selected ()
+ {
+ TextField tf = new ();
+
+ var selectingCount = 0;
+ tf.Selecting += (sender, args) => selectingCount++;
+
+ Toplevel top = new ();
+ top.Add (tf);
+ tf.SetFocus ();
+ top.NewKeyDownEvent (Key.Enter);
+
+ Assert.Equal (0, selectingCount);
+
+ top.Dispose ();
+ }
+
+ [Fact]
+ public void Enter_Raises_Accepted ()
+ {
+ TextField tf = new ();
+
+ var acceptedCount = 0;
+ tf.Accepting += (sender, args) => acceptedCount++;
+
+ Toplevel top = new ();
+ top.Add (tf);
+ tf.SetFocus ();
+ top.NewKeyDownEvent (Key.Enter);
+
+ Assert.Equal (1, acceptedCount);
+
+ top.Dispose ();
+ }
+
+ [Fact]
+ public void HotKey_Command_SetsFocus ()
+ {
+ var view = new TextField ();
+
+ view.CanFocus = true;
+ Assert.False (view.HasFocus);
+ view.InvokeCommand (Command.HotKey);
+ Assert.True (view.HasFocus);
+ }
+
+ [Fact]
+ public void HotKey_Command_Does_Not_Accept ()
+ {
+ var view = new TextField ();
+ var accepted = false;
+ view.Accepting += OnAccept;
+ view.InvokeCommand (Command.HotKey);
+
+ Assert.False (accepted);
+
+ return;
+
+ void OnAccept (object sender, CommandEventArgs e) { accepted = true; }
+ }
+
+ [Fact]
+ public void Accepted_Command_Fires_Accept ()
+ {
+ var view = new TextField ();
+
+ var accepted = false;
+ view.Accepting += Accept;
+ view.InvokeCommand (Command.Accept);
+ Assert.True (accepted);
+
+ return;
+
+ void Accept (object sender, CommandEventArgs e) { accepted = true; }
+ }
+
+ [Fact]
+ public void Accepted_No_Handler_Enables_Default_Button_Accept ()
+ {
+ var superView = new Window
+ {
+ Id = "superView"
+ };
+
+ var tf = new TextField
+ {
+ Id = "tf"
+ };
+
+ var button = new Button
+ {
+ Id = "button",
+ IsDefault = true
+ };
+
+ superView.Add (tf, button);
+
+ var buttonAccept = 0;
+ button.Accepting += ButtonAccept;
+
+ tf.SetFocus ();
+ Assert.True (tf.HasFocus);
+
+ superView.NewKeyDownEvent (Key.Enter);
+ Assert.Equal (1, buttonAccept);
+
+ button.SetFocus ();
+ superView.NewKeyDownEvent (Key.Enter);
+ Assert.Equal (2, buttonAccept);
+
+ return;
+
+ void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; }
+ }
+
+ [Fact]
+ public void Accepted_Cancel_Event_HandlesCommand ()
+ {
+ //var super = new View ();
+ var view = new TextField ();
+
+ //super.Add (view);
+
+ //var superAcceptedInvoked = false;
+
+ var tfAcceptedInvoked = false;
+ var handle = false;
+ view.Accepting += TextViewAccept;
+ Assert.False (view.InvokeCommand (Command.Accept));
+ Assert.True (tfAcceptedInvoked);
+
+ tfAcceptedInvoked = false;
+ handle = true;
+ view.Accepting += TextViewAccept;
+ Assert.True (view.InvokeCommand (Command.Accept));
+ Assert.True (tfAcceptedInvoked);
+
+ return;
+
+ void TextViewAccept (object sender, CommandEventArgs e)
+ {
+ tfAcceptedInvoked = true;
+ e.Cancel = handle;
+ }
+ }
+
+ [Fact]
+ public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility ()
+ {
+ var top = new Toplevel ();
+ var tf = new TextField { Width = 10 };
+ top.Add (tf);
+
+ Exception exception = Record.Exception (() => tf.SetFocus ());
+ Assert.Null (exception);
+ }
+
+ [Fact]
+ public void Backspace_From_End ()
+ {
+ var tf = new TextField { Text = "ABC" };
+ tf.SetFocus ();
+ Assert.Equal ("ABC", tf.Text);
+ tf.BeginInit ();
+ tf.EndInit ();
+
+ Assert.Equal (3, tf.CursorPosition);
+
+ // now delete the C
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("AB", tf.Text);
+ Assert.Equal (2, tf.CursorPosition);
+
+ // then delete the B
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("A", tf.Text);
+ Assert.Equal (1, tf.CursorPosition);
+
+ // then delete the A
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("", tf.Text);
+ Assert.Equal (0, tf.CursorPosition);
+ }
+
+ [Fact]
+ public void Backspace_From_Middle ()
+ {
+ var tf = new TextField { Text = "ABC" };
+ tf.SetFocus ();
+ tf.CursorPosition = 2;
+ Assert.Equal ("ABC", tf.Text);
+
+ // now delete the B
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("AC", tf.Text);
+
+ // then delete the A
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("C", tf.Text);
+
+ // then delete nothing
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("C", tf.Text);
+
+ // now delete the C
+ tf.CursorPosition = 1;
+ tf.NewKeyDownEvent (Key.Backspace);
+ Assert.Equal ("", tf.Text);
+ }
+
+ [Fact]
+ public void KeyDown_Handled_Prevents_Input ()
+ {
+ var tf = new TextField ();
+ tf.KeyDown += HandleJKey;
+
+ tf.NewKeyDownEvent (Key.A);
+ Assert.Equal ("a", tf.Text);
+
+ // SuppressKey suppresses the 'j' key
+ tf.NewKeyDownEvent (Key.J);
+ Assert.Equal ("a", tf.Text);
+
+ tf.KeyDown -= HandleJKey;
+
+ // Now that the delegate has been removed we can type j again
+ tf.NewKeyDownEvent (Key.J);
+ Assert.Equal ("aj", tf.Text);
+
+ return;
+
+ void HandleJKey (object s, Key arg)
+ {
+ if (arg.AsRune == new Rune ('j'))
+ {
+ arg.Handled = true;
+ }
+ }
+ }
+
+ [InlineData ("a")] // Lower than selection
+ [InlineData ("aaaaaaaaaaa")] // Greater than selection
+ [InlineData ("aaaa")] // Equal than selection
+ [Theory]
+ public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText)
+ {
+ var tf = new TextField ();
+ tf.Text = "fish";
+ tf.CursorPosition = tf.Text.Length;
+
+ tf.NewKeyDownEvent (Key.CursorLeft);
+
+ tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
+ tf.NewKeyDownEvent (Key.CursorLeft.WithShift);
+
+ Assert.Equal (1, tf.CursorPosition);
+ Assert.Equal (2, tf.SelectedLength);
+ Assert.Equal ("is", tf.SelectedText);
+
+ tf.Text = newText;
+ tf.CursorPosition = tf.Text.Length;
+
+ Assert.Equal (newText.Length, tf.CursorPosition);
+ Assert.Equal (0, tf.SelectedLength);
+ Assert.Null (tf.SelectedText);
+ }
+
+ [Fact]
+ public void SpaceHandling ()
+ {
+ var tf = new TextField { Width = 10, Text = " " };
+
+ var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked };
+
+ tf.NewMouseEvent (ev);
+ Assert.Equal (1, tf.SelectedLength);
+
+ ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked };
+
+ tf.NewMouseEvent (ev);
+ Assert.Equal (1, tf.SelectedLength);
+ }
+
+ [Fact]
+ public void WordBackward_WordForward_Mixed ()
+ {
+ var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" };
+ tf.BeginInit ();
+ tf.EndInit ();
+
+ tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+ Assert.Equal (15, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+ Assert.Equal (12, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+ Assert.Equal (10, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+ Assert.Equal (5, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl);
+ Assert.Equal (0, tf.CursorPosition);
+
+ tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+ Assert.Equal (5, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+ Assert.Equal (10, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+ Assert.Equal (12, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+ Assert.Equal (15, tf.CursorPosition);
+ tf.NewKeyDownEvent (Key.CursorRight.WithCtrl);
+ Assert.Equal (22, tf.CursorPosition);
+ }
+
+ [Fact]
+ public void WordBackward_WordForward_SelectedText_With_Accent ()
+ {
+ var text = "Les Misérables movie.";
+ var tf = new TextField { Width = 30, Text = text };
+
+ Assert.Equal (21, text.Length);
+ Assert.Equal (21, tf.Text.GetRuneCount ());
+ Assert.Equal (21, tf.Text.GetColumns ());
+
+ List runes = tf.Text.ToRuneList ();
+ Assert.Equal (21, runes.Count);
+ Assert.Equal (21, tf.Text.Length);
+
+ for (var i = 0; i < runes.Count; i++)
+ {
+ char cs = text [i];
+ var cus = (char)runes [i].Value;
+ Assert.Equal (cs, cus);
+ }
+
+ var idx = 15;
+ Assert.Equal ('m', text [idx]);
+ Assert.Equal ('m', (char)runes [idx].Value);
+ Assert.Equal ("m", runes [idx].ToString ());
+
+ Assert.True (
+ tf.NewMouseEvent (
+ new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
+ )
+ );
+ Assert.Equal ("movie.", tf.SelectedText);
+
+ Assert.True (
+ tf.NewMouseEvent (
+ new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf }
+ )
+ );
+ Assert.Equal ("movie.", tf.SelectedText);
+ }
+
+ [Fact]
+ public void Autocomplete_Popup_Added_To_SuperView_On_Init ()
+ {
+ View superView = new ()
+ {
+ CanFocus = true
+ };
+
+ TextField t = new ();
+
+ superView.Add (t);
+ Assert.Single (superView.SubViews);
+
+ superView.BeginInit ();
+ superView.EndInit ();
+
+ Assert.Equal (2, superView.SubViews.Count);
+ }
+
+ [Fact]
+ public void Autocomplete__Added_To_SuperView_On_Add ()
+ {
+ View superView = new ()
+ {
+ CanFocus = true,
+ Id = "superView"
+ };
+
+ superView.BeginInit ();
+ superView.EndInit ();
+ Assert.Empty (superView.SubViews);
+
+ TextField t = new ()
+ {
+ Id = "t"
+ };
+
+ superView.Add (t);
+
+ Assert.Equal (2, superView.SubViews.Count);
+ }
+
+ [Fact]
+ public void Right_CursorAtEnd_WithSelection_ShouldClearSelection ()
+ {
+ var tf = new TextField
+ {
+ Text = "Hello"
+ };
+ tf.SetFocus ();
+ tf.SelectAll ();
+ tf.CursorPosition = 5;
+
+ // When there is selected text and the cursor is at the end of the text field
+ Assert.Equal ("Hello", tf.SelectedText);
+
+ // Pressing right should not move focus, instead it should clear selection
+ Assert.True (tf.NewKeyDownEvent (Key.CursorRight));
+ Assert.Null (tf.SelectedText);
+
+ // Now that the selection is cleared another right keypress should move focus
+ Assert.False (tf.NewKeyDownEvent (Key.CursorRight));
+ }
+
+ [Fact]
+ public void Left_CursorAtStart_WithSelection_ShouldClearSelection ()
+ {
+ var tf = new TextField
+ {
+ Text = "Hello"
+ };
+ tf.SetFocus ();
+
+ tf.CursorPosition = 2;
+ Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
+ Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift));
+
+ // When there is selected text and the cursor is at the start of the text field
+ Assert.Equal ("He", tf.SelectedText);
+
+ // Pressing left should not move focus, instead it should clear selection
+ Assert.True (tf.NewKeyDownEvent (Key.CursorLeft));
+ Assert.Null (tf.SelectedText);
+
+ // When clearing selected text with left the cursor should be at the start of the selection
+ Assert.Equal (0, tf.CursorPosition);
+
+ // Now that the selection is cleared another left keypress should move focus
+ Assert.False (tf.NewKeyDownEvent (Key.CursorLeft));
+ }
+
+ [Fact]
+ public void Autocomplete_Visible_False_By_Default ()
+ {
+ View superView = new ()
+ {
+ CanFocus = true
+ };
+
+ TextField t = new ();
+
+ superView.Add (t);
+ superView.BeginInit ();
+ superView.EndInit ();
+
+ Assert.Equal (2, superView.SubViews.Count);
+
+ Assert.True (t.Visible);
+ Assert.False (t.Autocomplete.Visible);
+ }
+
+ [Fact]
+ public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair ()
+ {
+ var tf = new TextField ();
+
+ //📄 == \ud83d\udcc4 == \U0001F4C4
+ // � == Rune.ReplacementChar
+ tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d");
+ Assert.Equal ("aA,;📄📄��", tf.Text);
+ }
+
+ [Fact]
+ public void PositionCursor_Respect_GetColumns ()
+ {
+ var tf = new TextField { Width = 5 };
+ tf.BeginInit ();
+ tf.EndInit ();
+
+ tf.NewKeyDownEvent (new ("📄"));
+ Assert.Equal (1, tf.CursorPosition);
+ Assert.Equal (new (2, 0), tf.PositionCursor ());
+ Assert.Equal ("📄", tf.Text);
+
+ tf.NewKeyDownEvent (new (KeyCode.A));
+ Assert.Equal (2, tf.CursorPosition);
+ Assert.Equal (new (3, 0), tf.PositionCursor ());
+ Assert.Equal ("📄a", tf.Text);
+ }
+}