Browse Source

Merge branch 'v2_develop' of tig:gui-cs/Terminal.Gui into v2_develop

Tig 4 months ago
parent
commit
f84c528449

+ 31 - 0
Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs

@@ -0,0 +1,31 @@
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
+
+[MemoryDiagnoser]
+// Hide useless column from results.
+[HideColumns ("writer")]
+public class CSI_SetVsWrite
+{
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (TextWriterSource))]
+    public TextWriter Set (TextWriter writer)
+    {
+        writer.Write (Tui.EscSeqUtils.CSI_SetCursorPosition (1, 1));
+        return writer;
+    }
+
+    [Benchmark]
+    [ArgumentsSource (nameof (TextWriterSource))]
+    public TextWriter Write (TextWriter writer)
+    {
+        Tui.EscSeqUtils.CSI_WriteCursorPosition (writer, 1, 1);
+        return writer;
+    }
+
+    public static IEnumerable<object> TextWriterSource ()
+    {
+        return [StringWriter.Null];
+    }
+}

+ 18 - 18
Directory.Packages.props

@@ -7,41 +7,41 @@
 		<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
 
 		<PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
-		<PackageVersion Include="JetBrains.Annotations" Version="[2024.2.0,)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
-		<PackageVersion Include="System.IO.Abstractions" Version="[21.0.22,22)" />
+		<PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.Extensions.Logging" Version="[9.0.2,10)" />
+		<PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
 		<PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
 		<PackageVersion Include="Wcwidth" Version="[2,3)" />
 
-		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" />
+		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
 		<PackageVersion Include="Serilog" Version="4.2.0" />
 		<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
 		<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
 		<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
-		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.5,4)" />
+		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.7,4)" />
 		<PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
 		<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
 		<PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />
 
 		<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
 
-		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.2.2,9)" />
-		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[8,9)" />	
-		<PackageVersion Include="ReactiveUI" Version="[20.1.1,21)" />
+		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.4.0,9)" />
+		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[9.0.2,10)" />	
+		<PackageVersion Include="ReactiveUI" Version="[20.1.63,21)" />
 		<PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" />
-		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[1.0.3,2)"/>	
+		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[2.1.8,3)"/>	
 
-		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
-		<PackageVersion Include="Moq" Version="[4.20.70,5)" />
-		<PackageVersion Include="ReportGenerator" Version="[5.3.8,6)" />
-		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.29,22)" />
-		<PackageVersion Include="xunit" Version="[2.9.0,3)" />
+		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.13,18)" />
+		<PackageVersion Include="Moq" Version="[4.20.72,5)" />
+		<PackageVersion Include="ReportGenerator" Version="[5.4.4,6)" />
+		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[22.0.11,23)" />
+		<PackageVersion Include="xunit" Version="[2.9.3,3)" />
 		<PackageVersion Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
 		<PackageVersion Include="xunit.runner.visualstudio" Version="[2.8.2,3)"/> 
-		<PackageVersion Include="coverlet.collector" Version="[6.0.2,7)" />
+		<PackageVersion Include="coverlet.collector" Version="[6.0.4,7)" />
 		
 	</ItemGroup>
 

+ 0 - 1
ReactiveExample/LoginViewModel.cs

@@ -40,7 +40,6 @@ public partial class LoginViewModel : ReactiveObject
 
     public LoginViewModel ()
     {
-        InitializeCommands ();
         IObservable<bool> canLogin = this.WhenAnyValue
             (
                 x => x.Username,

+ 3 - 3
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -217,7 +217,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                         if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
                         {
                             // Just add this mark to the list
-                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                            Contents [Row, Col - 1].AddCombiningMark (rune);
 
                             // Ignore. Don't move to next column (let the driver figure out what to do).
                         }
@@ -240,7 +240,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                             else
                             {
                                 // It didn't normalize. Add it to the Cell to left's CM list
-                                Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                                Contents [Row, Col - 1].AddCombiningMark (rune);
 
                                 // Ignore. Don't move to next column (let the driver figure out what to do).
                             }
@@ -298,7 +298,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                         else if (!Clip.Contains (Col, Row))
                         {
                             // Our 1st column is outside the clip, so we can't display a wide character.
-                            Contents [Row, Col+1].Rune = Rune.ReplacementChar;
+                            Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
                         }
                         else
                         {

+ 29 - 3
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -1,5 +1,5 @@
 #nullable enable
-using Terminal.Gui.ConsoleDrivers;
+using System.Globalization;
 using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
 
 namespace Terminal.Gui;
@@ -154,13 +154,13 @@ public static class EscSeqUtils
     /// <summary>
     ///     Control sequence for disabling mouse events.
     /// </summary>
-    public static string CSI_DisableMouseEvents { get; set; } =
+    public static readonly string CSI_DisableMouseEvents =
         CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
 
     /// <summary>
     ///     Control sequence for enabling mouse events.
     /// </summary>
-    public static string CSI_EnableMouseEvents { get; set; } =
+    public static readonly string CSI_EnableMouseEvents =
         CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
 
     /// <summary>
@@ -1688,6 +1688,32 @@ public static class EscSeqUtils
         builder.Append ($"{CSI}{row};{col}H");
     }
 
+    /// <summary>
+    ///     ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
+    ///     of the y line
+    /// </summary>
+    /// <param name="writer">TextWriter where to write the cursor position sequence.</param>
+    /// <param name="row">Origin is (1,1).</param>
+    /// <param name="col">Origin is (1,1).</param>
+    public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col)
+    {
+        const int maxInputBufferSize =
+            // CSI (2) + ';' + 'H'
+            4 +
+            // row + col (2x int sign + int max value)
+            2 + 20;
+        Span<char> buffer = stackalloc char[maxInputBufferSize];
+        if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten))
+        {
+            string tooLongCursorPositionSequence = $"{CSI}{row};{col}H";
+            throw new InvalidOperationException (
+                $"{nameof(CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length}).");
+        }
+
+        ReadOnlySpan<char> cursorPositionSequence = buffer[..charsWritten];
+        writer.Write (cursorPositionSequence);
+    }
+
     //ESC [ <y> ; <x> f - HVP     Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
     //ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
     //ESC [ u - ANSISYSRC       Restore Cursor – Ansi.sys emulation	**With no parameters, performs a restore cursor operation like DECRC

+ 1 - 1
Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs

@@ -11,7 +11,7 @@ public interface IConsoleOutput : IDisposable
     ///     <see cref="IOutputBuffer"/> overload.
     /// </summary>
     /// <param name="text"></param>
-    void Write (string text);
+    void Write (ReadOnlySpan<char> text);
 
     /// <summary>
     ///     Write the contents of the <paramref name="buffer"/> to the console

+ 29 - 21
Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs

@@ -34,7 +34,10 @@ public class NetOutput : IConsoleOutput
     }
 
     /// <inheritdoc/>
-    public void Write (string text) { Console.Write (text); }
+    public void Write (ReadOnlySpan<char> text)
+    {
+        Console.Out.Write (text);
+    }
 
     /// <inheritdoc/>
     public void Write (IOutputBuffer buffer)
@@ -57,6 +60,9 @@ public class NetOutput : IConsoleOutput
         CursorVisibility? savedVisibility = _cachedCursorVisibility;
         SetCursorVisibility (CursorVisibility.Invisible);
 
+        const int maxCharsPerRune = 2;
+        Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
+
         for (int row = top; row < rows; row++)
         {
             if (Console.WindowHeight < 1)
@@ -115,26 +121,28 @@ public class NetOutput : IConsoleOutput
                     {
                         redrawAttr = attr;
 
-                        output.Append (
-                                       EscSeqUtils.CSI_SetForegroundColorRGB (
-                                                                              attr.Foreground.R,
-                                                                              attr.Foreground.G,
-                                                                              attr.Foreground.B
-                                                                             )
-                                      );
-
-                        output.Append (
-                                       EscSeqUtils.CSI_SetBackgroundColorRGB (
-                                                                              attr.Background.R,
-                                                                              attr.Background.G,
-                                                                              attr.Background.B
-                                                                             )
-                                      );
+                        EscSeqUtils.CSI_AppendForegroundColorRGB (
+                            output,
+                            attr.Foreground.R,
+                            attr.Foreground.G,
+                            attr.Foreground.B
+                        );
+
+                        EscSeqUtils.CSI_AppendBackgroundColorRGB (
+                            output,
+                            attr.Background.R,
+                            attr.Background.G,
+                            attr.Background.B
+                        );
                     }
 
                     outputWidth++;
+
+                    // Avoid Rune.ToString() by appending the rune chars.
                     Rune rune = buffer.Contents [row, col].Rune;
-                    output.Append (rune);
+                    int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer);
+                    ReadOnlySpan<char> runeChars = runeBuffer[..runeCharsWritten];
+                    output.Append (runeChars);
 
                     if (buffer.Contents [row, col].CombiningMarks.Count > 0)
                     {
@@ -162,7 +170,7 @@ public class NetOutput : IConsoleOutput
             if (output.Length > 0)
             {
                 SetCursorPositionImpl (lastCol, row);
-                Console.Write (output);
+                Console.Out.Write (output);
             }
         }
 
@@ -171,7 +179,7 @@ public class NetOutput : IConsoleOutput
             if (!string.IsNullOrWhiteSpace (s.SixelData))
             {
                 SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
-                Console.Write (s.SixelData);
+                Console.Out.Write (s.SixelData);
             }
         }
 
@@ -185,7 +193,7 @@ public class NetOutput : IConsoleOutput
     private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
     {
         SetCursorPositionImpl (lastCol, row);
-        Console.Write (output);
+        Console.Out.Write (output);
         output.Clear ();
         lastCol += outputWidth;
         outputWidth = 0;
@@ -222,7 +230,7 @@ public class NetOutput : IConsoleOutput
 
         // + 1 is needed because non-Windows is based on 1 instead of 0 and
         // Console.CursorTop/CursorLeft isn't reliable.
-        Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
+        EscSeqUtils.CSI_WriteCursorPosition (Console.Out, row + 1, col + 1);
 
         return true;
     }

+ 2 - 2
Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs

@@ -164,7 +164,7 @@ public class OutputBuffer : IOutputBuffer
                         if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
                         {
                             // Just add this mark to the list
-                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                            Contents [Row, Col - 1].AddCombiningMark (rune);
 
                             // Ignore. Don't move to next column (let the driver figure out what to do).
                         }
@@ -187,7 +187,7 @@ public class OutputBuffer : IOutputBuffer
                             else
                             {
                                 // It didn't normalize. Add it to the Cell to left's CM list
-                                Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                                Contents [Row, Col - 1].AddCombiningMark (rune);
 
                                 // Ignore. Don't move to next column (let the driver figure out what to do).
                             }

+ 28 - 20
Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Buffers;
 using System.ComponentModel;
 using System.Runtime.InteropServices;
 using Microsoft.Extensions.Logging;
@@ -6,12 +7,13 @@ using static Terminal.Gui.WindowsConsole;
 
 namespace Terminal.Gui;
 
-internal class WindowsOutput : IConsoleOutput
+internal partial class WindowsOutput : IConsoleOutput
 {
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsole (
+    [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static partial bool WriteConsole (
         nint hConsoleOutput,
-        string lpbufer,
+        ReadOnlySpan<char> lpbufer,
         uint numberOfCharsToWriten,
         out uint lpNumberOfCharsWritten,
         nint lpReserved
@@ -84,7 +86,7 @@ internal class WindowsOutput : IConsoleOutput
         }
     }
 
-    public void Write (string str)
+    public void Write (ReadOnlySpan<char> str)
     {
         if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
         {
@@ -183,7 +185,6 @@ internal class WindowsOutput : IConsoleOutput
 
     public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
     {
-        var stringBuilder = new StringBuilder ();
 
         //Debug.WriteLine ("WriteToConsole");
 
@@ -213,10 +214,10 @@ internal class WindowsOutput : IConsoleOutput
         }
         else
         {
-            stringBuilder.Clear ();
+            StringBuilder stringBuilder = new();
 
             stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
-            stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
+            EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0);
 
             Attribute? prev = null;
 
@@ -227,8 +228,8 @@ internal class WindowsOutput : IConsoleOutput
                 if (attr != prev)
                 {
                     prev = attr;
-                    stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
-                    stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
+                    EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
+                    EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
                 }
 
                 if (info.Char != '\x1b')
@@ -247,14 +248,20 @@ internal class WindowsOutput : IConsoleOutput
             stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
             stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
 
-            var s = stringBuilder.ToString ();
+            // TODO: Potentially could stackalloc whenever reasonably small (<= 8 kB?) write buffer is needed.
+            char [] rentedWriteArray = ArrayPool<char>.Shared.Rent (minimumLength: stringBuilder.Length);
+            try
+            {
+                Span<char> writeBuffer = rentedWriteArray.AsSpan(0, stringBuilder.Length);
+                stringBuilder.CopyTo (0, writeBuffer, stringBuilder.Length);
 
-            // TODO: requires extensive testing if we go down this route
-            // If console output has changed
-            //if (s != _lastWrite)
-            //{
-            // supply console with the new content
-            result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
+                // Supply console with the new content.
+                result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero);
+            }
+            finally
+            {
+                ArrayPool<char>.Shared.Return (rentedWriteArray);
+            }
 
             foreach (SixelToRender sixel in Application.Sixel)
             {
@@ -297,9 +304,10 @@ internal class WindowsOutput : IConsoleOutput
     /// <inheritdoc/>
     public void SetCursorVisibility (CursorVisibility visibility)
     {
-        var sb = new StringBuilder ();
-        sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-        Write (sb.ToString ());
+        string cursorVisibilitySequence = visibility != CursorVisibility.Invisible
+            ? EscSeqUtils.CSI_ShowCursor
+            : EscSeqUtils.CSI_HideCursor;
+        Write (cursorVisibilitySequence);
     }
 
     private Point _lastCursorPosition;

+ 5 - 4
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

@@ -6,7 +6,7 @@ using Terminal.Gui.ConsoleDrivers;
 
 namespace Terminal.Gui;
 
-internal class WindowsConsole
+internal partial class WindowsConsole
 {
     private CancellationTokenSource? _inputReadyCancellationTokenSource;
     private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ());
@@ -926,10 +926,11 @@ internal class WindowsConsole
         ref SmallRect lpWriteRegion
     );
 
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsole (
+    [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static partial bool WriteConsole (
         nint hConsoleOutput,
-        string lpbufer,
+        ReadOnlySpan<char> lpbufer,
         uint NumberOfCharsToWriten,
         out uint lpNumberOfCharsWritten,
         nint lpReserved

+ 35 - 6
Terminal.Gui/Drawing/Cell.cs

@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+#nullable enable
+
+namespace Terminal.Gui;
 
 /// <summary>
 ///     Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
@@ -23,12 +25,12 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru
         get => _rune;
         set
         {
-            CombiningMarks.Clear ();
+            _combiningMarks?.Clear ();
             _rune = value;
         }
     }
 
-    private List<Rune> _combiningMarks;
+    private List<Rune>? _combiningMarks;
 
     /// <summary>
     ///     The combining marks for <see cref="Rune"/> that when combined makes this Cell a combining sequence. If
@@ -38,10 +40,37 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru
     ///     Only valid in the rare case where <see cref="Rune"/> is a combining sequence that could not be normalized to a
     ///     single Rune.
     /// </remarks>
-    internal List<Rune> CombiningMarks
+    internal IReadOnlyList<Rune> CombiningMarks
+    {
+        // PERFORMANCE: Downside of the interface return type is that List<T> struct enumerator cannot be utilized, i.e. enumerator is allocated.
+        // If enumeration is used heavily in the future then might be better to expose the List<T> Enumerator directly via separate mechanism.
+        get
+        {
+            // Avoid unnecessary list allocation.
+            if (_combiningMarks == null)
+            {
+                return Array.Empty<Rune> ();
+            }
+            return _combiningMarks;
+        }
+    }
+
+    /// <summary>
+    ///     Adds combining mark to the cell.
+    /// </summary>
+    /// <param name="combiningMark">The combining mark to add to the cell.</param>
+    internal void AddCombiningMark (Rune combiningMark)
+    {
+        _combiningMarks ??= [];
+        _combiningMarks.Add (combiningMark);
+    }
+
+    /// <summary>
+    ///     Clears combining marks of the cell.
+    /// </summary>
+    internal void ClearCombiningMarks ()
     {
-        get => _combiningMarks ?? [];
-        private set => _combiningMarks = value ?? [];
+        _combiningMarks?.Clear ();
     }
 
     /// <inheritdoc/>

+ 1 - 1
Terminal.Gui/View/Adornment/Margin.cs

@@ -48,7 +48,7 @@ public class Margin : Adornment
 
     internal void CacheClip ()
     {
-        if (Thickness != Thickness.Empty)
+        if (Thickness != Thickness.Empty && ShadowStyle != ShadowStyle.None)
         {
             // PERFORMANCE: How expensive are these clones?
             _cachedClip = GetClip ()?.Clone ();

+ 24 - 14
Terminal.Gui/View/View.Drawing.cs

@@ -27,6 +27,7 @@ public partial class View // Drawing APIs
             view.Draw (context);
         }
 
+        // Draw the margins (those whith Shadows) last to ensure they are drawn on top of the content.
         Margin.DrawMargins (viewsArray);
     }
 
@@ -57,9 +58,9 @@ public partial class View // Drawing APIs
         {
             // ------------------------------------
             // Draw the Border and Padding.
-            // Note Margin is special-cased and drawn in a separate pass to support
+            // Note Margin with a Shadow is special-cased and drawn in a separate pass to support
             // transparent shadows.
-            DoDrawBorderAndPadding (originalClip);
+            DoDrawAdornments (originalClip);
             SetClip (originalClip);
 
             // ------------------------------------
@@ -106,7 +107,7 @@ public partial class View // Drawing APIs
             // ------------------------------------
             // Re-draw the border and padding subviews
             // HACK: This is a hack to ensure that the border and padding subviews are drawn after the line canvas.
-            DoDrawBorderAndPaddingSubViews ();
+            DoDrawAdornmentsSubViews ();
 
             // ------------------------------------
             // Advance the diagnostics draw indicator
@@ -116,8 +117,8 @@ public partial class View // Drawing APIs
         }
 
         // ------------------------------------
-        // This causes the Margin to be drawn in a second pass
-        // PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop.
+        // This causes the Margin to be drawn in a second pass if it has a ShadowStyle
+        // PERFORMANCE: If there is a Margin w/ Shadow, it will be redrawn each iteration of the main loop.
         Margin?.CacheClip ();
 
         // ------------------------------------
@@ -131,8 +132,11 @@ public partial class View // Drawing APIs
 
     #region DrawAdornments
 
-    private void DoDrawBorderAndPaddingSubViews ()
+    private void DoDrawAdornmentsSubViews ()
     {
+
+        // NOTE: We do not support subviews of Margin?
+
         if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty)
         {
             // PERFORMANCE: Get the check for DrawIndicator out of this somehow.
@@ -164,7 +168,7 @@ public partial class View // Drawing APIs
         }
     }
 
-    private void DoDrawBorderAndPadding (Region? originalClip)
+    private void DoDrawAdornments (Region? originalClip)
     {
         if (this is Adornment)
         {
@@ -194,27 +198,28 @@ public partial class View // Drawing APIs
             // A SubView may add to the LineCanvas. This ensures any Adornment LineCanvas updates happen.
             Border?.SetNeedsDraw ();
             Padding?.SetNeedsDraw ();
+            Margin?.SetNeedsDraw ();
         }
 
-        if (OnDrawingBorderAndPadding ())
+        if (OnDrawingAdornments ())
         {
             return;
         }
 
         // TODO: add event.
 
-        DrawBorderAndPadding ();
+        DrawAdornments ();
     }
 
     /// <summary>
-    ///     Causes <see cref="Border"/> and <see cref="Padding"/> to be drawn.
+    ///     Causes <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> to be drawn.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         <see cref="Margin"/> is drawn in a separate pass.
+    ///         <see cref="Margin"/> is drawn in a separate pass if <see cref="ShadowStyle"/> is set.
     ///     </para>
     /// </remarks>
-    public void DrawBorderAndPadding ()
+    public void DrawAdornments ()
     {
         // We do not attempt to draw Margin. It is drawn in a separate pass.
 
@@ -230,6 +235,11 @@ public partial class View // Drawing APIs
             Padding?.Draw ();
         }
 
+
+        if (Margin is { } && Margin.Thickness != Thickness.Empty && Margin.ShadowStyle == ShadowStyle.None)
+        {
+            Margin?.Draw ();
+        }
     }
 
     private void ClearFrame ()
@@ -255,7 +265,7 @@ public partial class View // Drawing APIs
     ///     false (the default), this method will cause the <see cref="LineCanvas"/> be prepared to be rendered.
     /// </summary>
     /// <returns><see langword="true"/> to stop further drawing of the Adornments.</returns>
-    protected virtual bool OnDrawingBorderAndPadding () { return false; }
+    protected virtual bool OnDrawingAdornments () { return false; }
 
     #endregion DrawAdornments
 
@@ -635,7 +645,7 @@ public partial class View // Drawing APIs
     /// <summary>
     ///     Gets or sets whether this View will use it's SuperView's <see cref="LineCanvas"/> for rendering any
     ///     lines. If <see langword="true"/> the rendering of any borders drawn by this Frame will be done by its parent's
-    ///     SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawingBorderAndPadding"/> method will
+    ///     SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawingAdornments"/> method will
     ///     be
     ///     called to render the borders.
     /// </summary>

+ 1 - 1
Terminal.Gui/Views/GraphView/Annotations.cs

@@ -150,7 +150,7 @@ public class LegendAnnotation : View, IAnnotation
 
         if (BorderStyle != LineStyle.None)
         {
-            DrawBorderAndPadding ();
+            DrawAdornments ();
             RenderLineCanvas ();
         }
 

+ 30 - 12
Terminal.Gui/Views/Menu/ContextMenu.cs

@@ -104,6 +104,7 @@ public sealed class ContextMenu : IDisposable
         if (_menuBar is { })
         {
             _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
+            _container?.Remove (_menuBar);
         }
         Application.UngrabMouse ();
         _menuBar?.Dispose ();
@@ -177,16 +178,16 @@ public sealed class ContextMenu : IDisposable
         }
 
         MenuItems = menuItems;
-        _container = Application.Top;
+        _container = GetTopSuperView (Host);
         _container!.Closing += Container_Closing;
         _container.Deactivate += Container_Deactivate;
         _container.Disposing += Container_Disposing;
-        Rectangle frame = Application.Screen;
+        Rectangle viewport = _container.Viewport;
         Point position = Position;
 
         if (Host is { })
         {
-            Point pos = Host.ViewportToScreen (frame).Location;
+            Point pos = Host.Frame.Location;
             pos.Y += Host.Frame.Height > 0 ? Host.Frame.Height - 1 : 0;
 
             if (position != pos)
@@ -197,11 +198,11 @@ public sealed class ContextMenu : IDisposable
 
         Rectangle rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children);
 
-        if (rect.Right >= frame.Right)
+        if (rect.Right >= viewport.Right)
         {
-            if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
+            if (viewport.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
             {
-                position.X = frame.Right - rect.Width;
+                position.X = viewport.Right - rect.Width;
             }
             else if (ForceMinimumPosToZero)
             {
@@ -213,17 +214,17 @@ public sealed class ContextMenu : IDisposable
             position.X = 0;
         }
 
-        if (rect.Bottom >= frame.Bottom)
+        if (rect.Bottom >= viewport.Bottom)
         {
-            if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
+            if (viewport.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
             {
                 if (Host is null)
                 {
-                    position.Y = frame.Bottom - rect.Height - 1;
+                    position.Y = viewport.Bottom - rect.Height - 1;
                 }
                 else
                 {
-                    Point pos = Host.ViewportToScreen (frame).Location;
+                    Point pos = Host.Frame.Location;
                     position.Y = pos.Y - rect.Height - 1;
                 }
             }
@@ -251,12 +252,29 @@ public sealed class ContextMenu : IDisposable
         _menuBar._isContextMenuLoading = true;
         _menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
 
-        _menuBar.BeginInit ();
-        _menuBar.EndInit ();
+        _container.Add (_menuBar);
         IsShow = true;
         _menuBar.OpenMenu ();
     }
 
+    internal static Toplevel? GetTopSuperView (View? view)
+    {
+        if (view is Toplevel toplevel)
+        {
+            return toplevel;
+        }
+
+        for (View? sv = view?.SuperView; sv != null; sv = sv.SuperView)
+        {
+            if (sv is Toplevel top)
+            {
+                return top;
+            }
+        }
+
+        return (Toplevel?)view?.SuperView ?? Application.Top;
+    }
+
     private void Container_Closing (object? sender, ToplevelClosingEventArgs obj) { Hide (); }
     private void Container_Deactivate (object? sender, ToplevelEventArgs e) { Hide (); }
     private void Container_Disposing (object? sender, EventArgs e) { Dispose (); }

+ 1 - 1
Terminal.Gui/Views/Menu/Menu.cs

@@ -831,7 +831,7 @@ internal sealed class Menu : View
             return;
         }
 
-        DrawBorderAndPadding ();
+        DrawAdornments ();
         RenderLineCanvas ();
 
         // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework.

+ 29 - 31
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -73,7 +73,7 @@ public class MenuBar : View, IDesignable
         Y = 0;
         Width = Dim.Fill ();
         Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
-        Menus = new MenuBarItem [] { };
+        Menus = [];
 
         //CanFocus = true;
         _selected = -1;
@@ -111,9 +111,14 @@ public class MenuBar : View, IDesignable
                     Command.Cancel,
                     () =>
                     {
-                        CloseMenuBar ();
+                        if (IsMenuOpen)
+                        {
+                            CloseMenuBar ();
 
-                        return true;
+                            return true;
+                        }
+
+                        return false;
                     }
                    );
 
@@ -556,10 +561,10 @@ public class MenuBar : View, IDesignable
 
     private void CloseOtherOpenedMenuBar ()
     {
-        if (Application.Top is { })
+        if (SuperView is { })
         {
             // Close others menu bar opened
-            Menu? menu = Application.Top.SubViews.FirstOrDefault (v => v is Menu m && m.Host != this && m.Host.IsMenuOpen) as Menu;
+            Menu? menu = SuperView.SubViews.FirstOrDefault (v => v is Menu m && m.Host != this && m.Host.IsMenuOpen) as Menu;
             menu?.Host.CleanUp ();
         }
     }
@@ -595,7 +600,7 @@ public class MenuBar : View, IDesignable
             case false:
                 if (_openMenu is { })
                 {
-                    Application.Top?.Remove (_openMenu);
+                    SuperView?.Remove (_openMenu);
                 }
 
                 SetNeedsDraw ();
@@ -634,7 +639,7 @@ public class MenuBar : View, IDesignable
 
                     if (OpenCurrentMenu is { })
                     {
-                        Application.Top?.Remove (OpenCurrentMenu);
+                        SuperView?.Remove (OpenCurrentMenu);
                         if (Application.MouseGrabView == OpenCurrentMenu)
                         {
                             Application.UngrabMouse ();
@@ -822,7 +827,7 @@ public class MenuBar : View, IDesignable
 
                 if (_openMenu is { })
                 {
-                    Application.Top?.Remove (_openMenu);
+                    SuperView?.Remove (_openMenu);
                     if (Application.MouseGrabView == _openMenu)
                     {
                         Application.UngrabMouse ();
@@ -838,34 +843,23 @@ public class MenuBar : View, IDesignable
                     pos += Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + 2 : 0) + _leftPadding + _rightPadding;
                 }
 
-                var locationOffset = Point.Empty;
 
-                // if SuperView is null then it's from a ContextMenu
-                if (SuperView is null)
-                {
-                    locationOffset = GetScreenOffset ();
-                }
 
-                if (SuperView is { } && SuperView != Application.Top)
-                {
-                    locationOffset.X += SuperView.Border.Thickness.Left;
-                    locationOffset.Y += SuperView.Border.Thickness.Top;
-                }
 
                 _openMenu = new ()
                 {
                     Host = this,
-                    X = Frame.X + pos + locationOffset.X,
-                    Y = Frame.Y + 1 + locationOffset.Y,
+                    X = Frame.X + pos,
+                    Y = Frame.Y + 1,
                     BarItems = Menus [index],
                     Parent = null
                 };
                 OpenCurrentMenu = _openMenu;
                 OpenCurrentMenu._previousSubFocused = _openMenu;
 
-                if (Application.Top is { })
+                if (SuperView is { })
                 {
-                    Application.Top.Add (_openMenu);
+                    SuperView.Add (_openMenu);
                    // _openMenu.SetRelativeLayout (Application.Screen.Size);
                 }
                 else
@@ -894,13 +888,11 @@ public class MenuBar : View, IDesignable
 
                     if (!UseSubMenusSingleFrame)
                     {
-                        locationOffset = GetLocationOffset ();
-
                         OpenCurrentMenu = new ()
                         {
                             Host = this,
-                            X = last!.Frame.Left + last.Frame.Width + locationOffset.X,
-                            Y = last.Frame.Top + locationOffset.Y + last._currentChild,
+                            X = last!.Frame.Left + last.Frame.Width,
+                            Y = last.Frame.Top + last._currentChild + 1,
                             BarItems = subMenu,
                             Parent = last
                         };
@@ -931,7 +923,7 @@ public class MenuBar : View, IDesignable
 
                     OpenCurrentMenu._previousSubFocused = last._previousSubFocused;
                     _openSubMenu.Add (OpenCurrentMenu);
-                    Application.Top?.Add (OpenCurrentMenu);
+                    SuperView?.Add (OpenCurrentMenu);
 
                     if (!OpenCurrentMenu.IsInitialized)
                     {
@@ -1014,7 +1006,7 @@ public class MenuBar : View, IDesignable
         {
             foreach (Menu item in _openSubMenu)
             {
-                Application.Top!.Remove (item);
+                SuperView?.Remove (item);
                 if (Application.MouseGrabView == item)
                 {
                     Application.UngrabMouse ();
@@ -1263,7 +1255,7 @@ public class MenuBar : View, IDesignable
             if (_openSubMenu is { })
             {
                 menu = _openSubMenu [i];
-                Application.Top!.Remove (menu);
+                SuperView!.Remove (menu);
                 _openSubMenu.Remove (menu);
 
                 if (Application.MouseGrabView == menu)
@@ -1544,8 +1536,14 @@ public class MenuBar : View, IDesignable
 
                 if (me.View != current)
                 {
+                    View v = current;
                     Application.UngrabMouse ();
-                    View v = me.View;
+
+                    if (((Menu)me.View).Host.SuperView is { } && ((Menu)me.View).Host.SuperView!.InternalSubViews.Contains(me.View))
+                    {
+                        v = me.View;
+                    }
+
                     Application.GrabMouse (v);
                     MouseEventArgs nme;
 

+ 1 - 1
Terminal.Gui/Views/TileView.cs

@@ -173,7 +173,7 @@ public class TileView : View
 
     /// <summary>Overridden so no Frames get drawn</summary>
     /// <returns></returns>
-    protected override bool OnDrawingBorderAndPadding () { return true; }
+    protected override bool OnDrawingAdornments () { return true; }
 
     /// <inheritdoc/>
     protected override bool OnRenderingLineCanvas () { return false; }

+ 18 - 3
Tests/UnitTests/Input/EscSeqUtilsTests.cs

@@ -1,4 +1,4 @@
-using JetBrains.Annotations;
+using System.Text;
 using UnitTests;
 
 // ReSharper disable HeuristicUnreachableCode
@@ -685,7 +685,7 @@ public class EscSeqUtilsTests
         top.Add (view);
         Application.Begin (top);
 
-        Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
+        Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 });
 
         ClearAll ();
 
@@ -741,7 +741,7 @@ public class EscSeqUtilsTests
                                          // set Application.WantContinuousButtonPressedView to null
                                          view.WantContinuousButtonPressed = false;
 
-                                         Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
+                                         Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 });
 
                                          Application.RequestStop ();
                                      }
@@ -1548,6 +1548,21 @@ public class EscSeqUtilsTests
         Assert.Equal (result, cki);
     }
 
+    [Theory]
+    [InlineData (0, 0, $"{EscSeqUtils.CSI}0;0H")]
+    [InlineData (int.MaxValue, int.MaxValue, $"{EscSeqUtils.CSI}2147483647;2147483647H")]
+    [InlineData (int.MinValue, int.MinValue, $"{EscSeqUtils.CSI}-2147483648;-2147483648H")]
+    public void CSI_WriteCursorPosition_ReturnsCorrectEscSeq (int row, int col, string expected)
+    {
+        StringBuilder builder = new();
+        using StringWriter writer = new(builder);
+
+        EscSeqUtils.CSI_WriteCursorPosition (writer, row, col);
+
+        string actual = builder.ToString();
+        Assert.Equal (expected, actual);
+    }
+
     private void ClearAll ()
     {
         EscSeqRequests.Clear ();

+ 190 - 26
Tests/UnitTests/Views/ContextMenuTests.cs

@@ -530,10 +530,12 @@ public class ContextMenuTests (ITestOutputHelper output)
                                                       output
                                                      );
 
+        View menu = top.SubViews.First (v => v is Menu);
+
         Assert.True (
-                     top.SubViews.ElementAt (0)
+                     menu
                         .NewMouseEvent (
-                                        new MouseEventArgs { Position = new (0, 3), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (0) }
+                                        new MouseEventArgs { Position = new (0, 3), Flags = MouseFlags.ReportMousePosition, View = menu }
                                        )
                     );
         Application.RunIteration (ref rs);
@@ -578,10 +580,11 @@ public class ContextMenuTests (ITestOutputHelper output)
                                                       output
                                                      );
 
+        menu = top.SubViews.First (v => v is Menu);
         Assert.True (
-                     top.SubViews.ElementAt (0)
+                     menu
                         .NewMouseEvent (
-                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (0) }
+                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu }
                                        )
                     );
         Application.RunIteration (ref rs);
@@ -625,10 +628,11 @@ public class ContextMenuTests (ITestOutputHelper output)
                                                       output
                                                      );
 
+        menu = top.SubViews.First (v => v is Menu);
         Assert.True (
-                     top.SubViews.ElementAt (0)
+                     menu
                         .NewMouseEvent (
-                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (0) }
+                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu }
                                        )
                     );
         Application.RunIteration (ref rs);
@@ -669,10 +673,11 @@ public class ContextMenuTests (ITestOutputHelper output)
                                                       output
                                                      );
 
+        menu = top.SubViews.First (v => v is Menu);
         Assert.True (
-                     top.SubViews.ElementAt (0)
+                     menu
                         .NewMouseEvent (
-                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (0) }
+                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu }
                                        )
                     );
         Application.RunIteration (ref rs);
@@ -713,10 +718,11 @@ public class ContextMenuTests (ITestOutputHelper output)
                                                       output
                                                      );
 
+        menu = top.SubViews.First (v => v is Menu);
         Assert.True (
-                     top.SubViews.ElementAt (0)
+                     menu
                         .NewMouseEvent (
-                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (0) }
+                                        new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu }
                                        )
                     );
         Application.RunIteration (ref rs);
@@ -1223,7 +1229,8 @@ public class ContextMenuTests (ITestOutputHelper output)
         Toplevel top = new ();
         RunState rs = Application.Begin (top);
         cm.Show (menuItems);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top!.SubViews.ElementAt (0).Frame);
+        var menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
         Application.LayoutAndDraw ();
 
         DriverAssert.AssertDriverContentsWithFrameAre (
@@ -1241,8 +1248,10 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         var firstIteration = false;
         Application.RunIteration (ref rs, firstIteration);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
-        Assert.Equal (new Rectangle (5, 11, 15, 6), Application.Top.SubViews.ElementAt (1).Frame);
+        menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
+        menu = Application.Top!.SubViews.Last (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 15, 6), menu.Frame);
 
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                       @"
@@ -1259,7 +1268,8 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         firstIteration = false;
         Application.RunIteration (ref rs, firstIteration);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
+        menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
 
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                       @"
@@ -1316,7 +1326,10 @@ public class ContextMenuTests (ITestOutputHelper output)
         RunState rs = Application.Begin (top);
         cm.Show (menuItems);
 
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
+
+        var menu = Application.Top!.SubViews.First (v => v is Menu);
+
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
         Application.LayoutAndDraw ();
 
         DriverAssert.AssertDriverContentsWithFrameAre (
@@ -1333,7 +1346,8 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         var firstIteration = false;
         Application.RunIteration (ref rs, firstIteration);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
+        menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
 
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                       @"
@@ -1350,7 +1364,8 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         firstIteration = false;
         Application.RunIteration (ref rs, firstIteration);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
+        menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
 
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                       @"
@@ -1368,7 +1383,8 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         firstIteration = false;
         Application.RunIteration (ref rs, firstIteration);
-        Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.SubViews.ElementAt (0).Frame);
+        menu = Application.Top!.SubViews.First (v => v is Menu);
+        Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame);
 
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                       @"
@@ -1404,7 +1420,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button3Clicked });
         Assert.False (tf1.HasFocus);
         Assert.False (tf2.HasFocus);
-        Assert.Equal (5, win.SubViews.Count);
+        Assert.Equal (6, win.SubViews.Count);
         Assert.True (tf2.ContextMenu.MenuBar.IsMenuOpen);
         Assert.True (win.Focused is Menu);
         Assert.True (Application.MouseGrabView is MenuBar);
@@ -1414,7 +1430,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked });
         Assert.True (tf1.HasFocus);
         Assert.False (tf2.HasFocus);
-        Assert.Equal (4, win.SubViews.Count);
+        Assert.Equal (5, win.SubViews.Count);
 
         // The last context menu bar opened is always preserved
         Assert.NotNull (tf2.ContextMenu.MenuBar);
@@ -1426,7 +1442,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked });
         Assert.False (tf1.HasFocus);
         Assert.True (tf2.HasFocus);
-        Assert.Equal (4, win.SubViews.Count);
+        Assert.Equal (5, win.SubViews.Count);
 
         // The last context menu bar opened is always preserved
         Assert.NotNull (tf2.ContextMenu.MenuBar);
@@ -1710,7 +1726,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
         Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _));
         Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _));
-        Assert.Single (Application.Top!.SubViews);
+        Assert.Equal (2, Application.Top!.SubViews.Count);
         View [] menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _));
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.NoShift, out _));
@@ -1835,7 +1851,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.NoShift, out _));
         Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _));
         Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _));
-        Assert.Equal (3, Application.Top!.SubViews.Count);
+        Assert.Equal (4, Application.Top!.SubViews.Count);
         menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _));
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _));
@@ -1850,7 +1866,7 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         cm.Show (menuItems);
         Assert.True (cm.MenuBar.IsMenuOpen);
-        Assert.Equal (3, Application.Top!.SubViews.Count);
+        Assert.Equal (4, Application.Top!.SubViews.Count);
         menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray ();
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _));
         Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _));
@@ -1866,7 +1882,7 @@ public class ContextMenuTests (ITestOutputHelper output)
         Application.MainLoop!.RunIteration ();
         Assert.True (renameFile);
 
-        Assert.Single (Application.Top!.SubViews);
+        Assert.Equal (2, Application.Top!.SubViews.Count);
         Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _));
         Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _));
         Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _));
@@ -1938,4 +1954,152 @@ public class ContextMenuTests (ITestOutputHelper output)
 
         top.Dispose ();
     }
-}
+
+    [Theory]
+    [InlineData (1)]
+    [InlineData (2)]
+    [InlineData (3)]
+    [AutoInitShutdown]
+    public void Mouse_Pressed_Released_Clicked (int button)
+    {
+        var actionRaised = false;
+
+        var menuBar = new MenuBar
+        {
+            Menus =
+            [
+                new (
+                     "_File",
+                     new MenuItem []
+                     {
+                         new ("_New", string.Empty, () => actionRaised = true)
+                     })
+            ]
+        };
+        var cm = new ContextMenu ();
+
+        var menuItems = new MenuBarItem (
+                                         [
+                                             new ("_Rename File", string.Empty, () => actionRaised = true)
+                                         ]
+                                        );
+        var top = new Toplevel ();
+
+        top.MouseClick += (s, e) =>
+                          {
+                              if (e.Flags == cm.MouseFlags)
+                              {
+                                  cm.Position = new (e.Position.X, e.Position.Y);
+                                  cm.Show (menuItems);
+                                  e.Handled = true;
+                              }
+                          };
+
+        top.Add (menuBar);
+        Application.Begin (top);
+
+        // MenuBar
+        Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed });
+        Assert.True (menuBar.IsMenuOpen);
+
+        switch (button)
+        {
+            // Left Button
+            case 1:
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Pressed });
+                Assert.True (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Released });
+                Assert.True (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked });
+                Assert.False (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.True (actionRaised);
+                actionRaised = false;
+
+                break;
+            // Middle Button
+            case 2:
+            // Right Button
+            case 3:
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Pressed });
+                Assert.False (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Released });
+                Assert.False (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Clicked });
+                Assert.False (menuBar.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+
+                break;
+        }
+
+        // ContextMenu
+        Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 2), Flags = cm.MouseFlags });
+        Assert.False (menuBar.IsMenuOpen);
+        Assert.True (cm.MenuBar!.IsMenuOpen);
+
+        switch (button)
+        {
+            // Left Button
+            case 1:
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Pressed });
+                Assert.True (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Released });
+                Assert.True (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.True (actionRaised);
+                actionRaised = false;
+
+                break;
+            // Middle Button
+            case 2:
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Pressed });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Released });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Clicked });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+
+                break;
+            // Right Button
+            case 3:
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Pressed });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Released });
+                Assert.False (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+                Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Clicked });
+                // MouseFlags is the same as cm.MouseFlags. So the context menu is closed and reopened again
+                Assert.True (cm.MenuBar!.IsMenuOpen);
+                Application.MainLoop.RunIteration ();
+                Assert.False (actionRaised);
+
+                break;
+        }
+
+        top.Dispose ();
+    }
+}

+ 28 - 0
Tests/UnitTests/Views/MenuBarTests.cs

@@ -3734,6 +3734,34 @@ Edit
         top.Dispose ();
     }
 
+    [Fact]
+    [AutoInitShutdown]
+    public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False ()
+    {
+        var menu = new MenuBar
+        {
+            Menus =
+            [
+                new ("File", new MenuItem [] { new ("New", "", null) })
+            ],
+            CanFocus = true
+        };
+        var top = new Toplevel ();
+        top.Add (menu);
+        Application.Begin (top);
+
+        Assert.True (menu.CanFocus);
+        Assert.True (menu.NewKeyDownEvent (menu.Key));
+        Assert.True (menu.IsMenuOpen);
+
+        Assert.True (menu.NewKeyDownEvent (Key.Esc));
+        Assert.False (menu.IsMenuOpen);
+
+        Assert.False (menu.NewKeyDownEvent (Key.Esc));
+        Assert.False (menu.IsMenuOpen);
+        top.Dispose ();
+    }
+
     // Defines the expected strings for a Menu. Currently supports 
     //   - MenuBar with any number of MenuItems 
     //   - Each top-level MenuItem can have a SINGLE sub-menu

+ 2 - 2
UICatalog/Scenarios/DynamicMenuBar.cs

@@ -414,10 +414,10 @@ public class DynamicMenuBar : Scenario
                                 };
 
             var dialog = new Dialog
-                { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 22, Application.Screen.Height) };
+                { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 23, Application.Screen.Height) };
 
             Width = Dim.Fill ();
-            Height = Dim.Fill () - 1;
+            Height = Dim.Fill () - 2;
             dialog.Add (this);
             TextTitle.SetFocus ();
             TextTitle.CursorPosition = TextTitle.Text.Length;

+ 1 - 1
UICatalog/Scenarios/DynamicStatusBar.cs

@@ -204,7 +204,7 @@ public class DynamicStatusBar : Scenario
             var dialog = new Dialog { Title = "Enter the menu details.", Buttons = [btnOk, btnCancel], Height = Dim.Auto (DimAutoStyle.Content, 17, Application.Screen.Height) };
 
             Width = Dim.Fill ();
-            Height = Dim.Fill () - 1;
+            Height = Dim.Fill () - 2;
             dialog.Add (this);
             TextTitle.SetFocus ();
             TextTitle.CursorPosition = TextTitle.Text.Length;

+ 16 - 2
UICatalog/Scenarios/Generic.cs

@@ -18,7 +18,21 @@ public sealed class Generic : Scenario
             Title = GetQuitKeyAndName (),
         };
 
-        var button = new Button { Id = "button", X = Pos.Center (), Y = 1, Text = "_Press me!" };
+        FrameView frame = new ()
+        {
+            Height = Dim.Fill (),
+            Width = Dim.Fill (),
+            Title = "Frame"
+        };
+        appWindow.Add (frame);
+
+        var button = new Shortcut ()
+        {
+            Id = "button", 
+            X = Pos.Center (), 
+            Y = 1, 
+            Text = "_Press me!"
+        };
 
         button.Accepting += (s, e) =>
                             {
@@ -27,7 +41,7 @@ public sealed class Generic : Scenario
                                 MessageBox.ErrorQuery ("Error", "You pressed the button!", "_Ok");
                             };
 
-        appWindow.Add (button);
+        frame.Add (button);
 
         // Run - Start the application.
         Application.Run (appWindow);