diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs
index 1e037fee2..9f8e0a9b8 100644
--- a/Terminal.Gui/App/ApplicationImpl.Run.cs
+++ b/Terminal.Gui/App/ApplicationImpl.Run.cs
@@ -368,6 +368,8 @@ internal partial class ApplicationImpl
previousRunnable.RaiseIsModalChangedEvent (true);
}
+ Mouse?.UngrabMouse ();
+
runnable.RaiseIsRunningChangedEvent (false);
token.Result = runnable.Result;
diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs
index fe8c26d37..ce9e0a9b6 100644
--- a/Terminal.Gui/App/Mouse/MouseImpl.cs
+++ b/Terminal.Gui/App/Mouse/MouseImpl.cs
@@ -266,11 +266,17 @@ internal class MouseImpl : IMouse, IDisposable
///
public void GrabMouse (View? view)
{
- if (view is null || RaiseGrabbingMouseEvent (view))
+ if (RaiseGrabbingMouseEvent (view))
{
return;
}
+ if (view is null)
+ {
+ UngrabMouse();
+ return;
+ }
+
RaiseGrabbedMouseEvent (view);
// MouseGrabView is only set if the application is initialized.
diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs
index ac0a593a1..c170ff949 100644
--- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs
+++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs
@@ -1,4 +1,3 @@
-using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.ViewBase;
@@ -766,6 +765,17 @@ public partial class Border
}
}
+ ///
+ /// Cancels events during an active drag to prevent other views from
+ /// stealing the mouse grab mid-operation.
+ ///
+ ///
+ /// During an Arrange Mode drag ( has a value), Border owns the mouse grab and
+ /// must receive all mouse events until Button1Released. If another view (e.g., scrollbar, slider) were allowed
+ /// to grab the mouse, the drag would freeze, leaving Border in an inconsistent state with no cleanup.
+ /// Canceling follows the CWP pattern, ensuring Border maintains exclusive mouse control until it explicitly
+ /// releases via in .
+ ///
private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e)
{
if (App?.Mouse.MouseGrabView == this && _dragPosition.HasValue)
@@ -774,25 +784,14 @@ public partial class Border
}
}
- private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e)
- {
- if (App?.Mouse.MouseGrabView == this && _dragPosition.HasValue)
- {
- e.Cancel = true;
- }
- }
-
#endregion Mouse Support
-
-
///
protected override void Dispose (bool disposing)
{
if (App is { })
{
App.Mouse.GrabbingMouse -= Application_GrabbingMouse;
- App.Mouse.UnGrabbingMouse -= Application_UnGrabbingMouse;
}
_dragPosition = null;
diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs
index 65a38f811..3e996c270 100644
--- a/Terminal.Gui/ViewBase/Adornment/Border.cs
+++ b/Terminal.Gui/ViewBase/Adornment/Border.cs
@@ -111,7 +111,6 @@ public partial class Border : Adornment
if (App is { })
{
App.Mouse.GrabbingMouse += Application_GrabbingMouse;
- App.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse;
}
if (Parent is null)
diff --git a/Terminal.Gui/ViewBase/Runnable/Runnable.cs b/Terminal.Gui/Views/Runnable/Runnable.cs
similarity index 99%
rename from Terminal.Gui/ViewBase/Runnable/Runnable.cs
rename to Terminal.Gui/Views/Runnable/Runnable.cs
index d51363354..8d0332732 100644
--- a/Terminal.Gui/ViewBase/Runnable/Runnable.cs
+++ b/Terminal.Gui/Views/Runnable/Runnable.cs
@@ -1,4 +1,4 @@
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.Views;
///
/// Base implementation of for views that can be run as blocking sessions without returning a result.
diff --git a/Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs b/Terminal.Gui/Views/Runnable/RunnableTResult.cs
similarity index 98%
rename from Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs
rename to Terminal.Gui/Views/Runnable/RunnableTResult.cs
index 44245b235..872f1ec3a 100644
--- a/Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs
+++ b/Terminal.Gui/Views/Runnable/RunnableTResult.cs
@@ -1,4 +1,4 @@
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.Views;
///
/// Base implementation of for views that can be run as blocking sessions.
diff --git a/Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs b/Terminal.Gui/Views/Runnable/RunnableWrapper.cs
similarity index 98%
rename from Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs
rename to Terminal.Gui/Views/Runnable/RunnableWrapper.cs
index bf10b4c0f..fde3c48c1 100644
--- a/Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs
+++ b/Terminal.Gui/Views/Runnable/RunnableWrapper.cs
@@ -1,4 +1,4 @@
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.Views;
///
/// Wraps any to make it runnable with a typed result, similar to how
diff --git a/Terminal.Gui/ViewBase/Runnable/ViewRunnableExtensions.cs b/Terminal.Gui/Views/Runnable/ViewRunnableExtensions.cs
similarity index 99%
rename from Terminal.Gui/ViewBase/Runnable/ViewRunnableExtensions.cs
rename to Terminal.Gui/Views/Runnable/ViewRunnableExtensions.cs
index 7b12bb055..ea9eca269 100644
--- a/Terminal.Gui/ViewBase/Runnable/ViewRunnableExtensions.cs
+++ b/Terminal.Gui/Views/Runnable/ViewRunnableExtensions.cs
@@ -1,4 +1,4 @@
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.Views;
///
/// Extension methods for making any runnable with typed results.
diff --git a/Tests/UnitTests/View/Mouse/MouseTests.cs b/Tests/UnitTests/View/Mouse/MouseTests.cs
index c832af152..f2c17e186 100644
--- a/Tests/UnitTests/View/Mouse/MouseTests.cs
+++ b/Tests/UnitTests/View/Mouse/MouseTests.cs
@@ -49,8 +49,10 @@ public class MouseTests : TestsAllViews
Application.RaiseMouseEvent (new () { ScreenPosition = new (xy + 1, xy + 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
AutoInitShutdownAttribute.RunIteration ();
-
Assert.Equal (expectedMoved, new Point (5, 5) == testView.Frame.Location);
+ // The above grabbed the mouse. Need to ungrab.
+ Application.Mouse.UngrabMouse ();
+
top.Dispose ();
}
diff --git a/Tests/UnitTests/View/ViewCommandTests.cs b/Tests/UnitTests/View/ViewCommandTests.cs
index f77f36af1..ae4fe2e12 100644
--- a/Tests/UnitTests/View/ViewCommandTests.cs
+++ b/Tests/UnitTests/View/ViewCommandTests.cs
@@ -152,6 +152,9 @@ public class ViewCommandTests
Assert.Equal (1, btnAcceptedCount);
Assert.Equal (0, wAcceptedCount);
+ // The above grabbed the mouse. Need to ungrab.
+ Application.Mouse.UngrabMouse ();
+
w.Dispose ();
Application.ResetState (true);
}
diff --git a/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs b/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs
index 6d25610bd..2085e70a8 100644
--- a/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs
+++ b/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs
@@ -1,6 +1,6 @@
using Xunit.Abstractions;
-namespace ApplicationTests;
+namespace ApplicationTests.Navigation;
public class ApplicationNavigationTests (ITestOutputHelper output)
{
diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
index 048c683d5..1e542dc46 100644
--- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
+++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
@@ -5,6 +5,66 @@ namespace ApplicationTests;
public class ApplicationImplTests
{
+
+ [Fact]
+ public void Internal_Properties_Correct ()
+ {
+ IApplication app = Application.Create ();
+ app.Init ("fake");
+
+ Assert.True (app.Initialized);
+ Assert.Null (app.TopRunnableView);
+ SessionToken? rs = app.Begin (new Runnable ());
+ Assert.Equal (app.TopRunnable, rs!.Runnable);
+ Assert.Null (app.Mouse.MouseGrabView); // public
+
+ app.Dispose ();
+ }
+
+
+ #region DisposeTests
+
+ [Fact]
+ public async Task Dispose_Allows_Async ()
+ {
+ var isCompletedSuccessfully = false;
+
+ async Task TaskWithAsyncContinuation ()
+ {
+ await Task.Yield ();
+ await Task.Yield ();
+
+ isCompletedSuccessfully = true;
+ }
+
+ IApplication app = Application.Create ();
+ app.Dispose ();
+
+ Assert.False (isCompletedSuccessfully);
+ await TaskWithAsyncContinuation ();
+ Thread.Sleep (100);
+ Assert.True (isCompletedSuccessfully);
+ }
+
+ [Fact]
+ public void Dispose_Resets_SyncContext ()
+ {
+ IApplication app = Application.Create ();
+ app.Dispose ();
+ Assert.Null (SynchronizationContext.Current);
+ }
+
+ [Fact]
+ public void Dispose_Alone_Does_Nothing ()
+ {
+ IApplication app = Application.Create ();
+ app.Dispose ();
+ }
+
+
+ #endregion
+
+
///
/// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
///
@@ -44,21 +104,6 @@ public class ApplicationImplTests
.Verifiable (Times.Once);
}
- [Fact]
- public void Init_CreatesKeybindings ()
- {
- IApplication app = NewMockedApplicationImpl ();
-
- app.Keyboard.KeyBindings.Clear ();
-
- Assert.Empty (app.Keyboard.KeyBindings.GetBindings ());
-
- app.Init ("fake");
-
- Assert.NotEmpty (app.Keyboard.KeyBindings.GetBindings ());
-
- app.Dispose ();
- }
[Fact]
public void NoInitThrowOnRun ()
@@ -480,81 +525,4 @@ public class ApplicationImplTests
Assert.Null (v2.TopRunnableView);
Assert.Empty (v2.SessionStack!);
}
-
- [Fact]
- public void Init_Begin_End_Cleans_Up ()
- {
- IApplication? app = Application.Create ();
-
- SessionToken? newSessionToken = null;
-
- EventHandler newSessionTokenFn = (s, e) =>
- {
- Assert.NotNull (e.State);
- newSessionToken = e.State;
- };
- app.SessionBegun += newSessionTokenFn;
-
- Runnable runnable = new ();
- SessionToken sessionToken = app.Begin (runnable)!;
- Assert.NotNull (sessionToken);
- Assert.NotNull (newSessionToken);
- Assert.Equal (sessionToken, newSessionToken);
-
- // Assert.Equal (runnable, Application.TopRunnable);
-
- app.SessionBegun -= newSessionTokenFn;
- app.End (newSessionToken);
-
- Assert.Null (app.TopRunnable);
- Assert.Null (app.Driver);
-
- runnable.Dispose ();
- }
-
- [Fact]
- public void Run_RequestStop_Stops ()
- {
- IApplication? app = Application.Create ();
- app.Init ("fake");
-
- var top = new Runnable ();
- SessionToken? sessionToken = app.Begin (top);
- Assert.NotNull (sessionToken);
-
- app.Iteration += OnApplicationOnIteration;
- app.Run (top);
- app.Iteration -= OnApplicationOnIteration;
-
- top.Dispose ();
-
- return;
-
- void OnApplicationOnIteration (object? s, EventArgs a) { app.RequestStop (); }
- }
-
- [Fact]
- public void Run_T_Init_Driver_Cleared_with_Runnable_Throws ()
- {
- IApplication? app = Application.Create ();
-
- app.Init ("fake");
- app.Driver = null;
-
- app.StopAfterFirstIteration = true;
-
- // Init has been called, but Driver has been set to null. Bad.
- Assert.Throws (() => app.Run ());
- }
-
- [Fact]
- public void Init_Unbalanced_Throws ()
- {
- IApplication? app = Application.Create ();
- app.Init ("fake");
-
- Assert.Throws (() =>
- app.Init ("fake")
- );
- }
}
diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs
deleted file mode 100644
index e28381940..000000000
--- a/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs
+++ /dev/null
@@ -1,510 +0,0 @@
-#nullable enable
-using Xunit.Abstractions;
-
-namespace ApplicationTests;
-
-///
-/// Parallelizable tests for IApplication that don't require the main event loop.
-/// Tests using the modern non-static IApplication API.
-///
-public class ApplicationTests (ITestOutputHelper output)
-{
- private readonly ITestOutputHelper _output = output;
-
-
- [Fact]
- public void Begin_Null_Runnable_Throws ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- // Test null Runnable
- Assert.Throws (() => app.Begin (null!));
-
- app.Dispose ();
- }
-
- [Fact]
- public void Begin_Sets_Application_Top_To_Console_Size ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- Assert.Null (app.TopRunnableView);
- app.Driver!.SetScreenSize (80, 25);
- Runnable top = new ();
- SessionToken? token = app.Begin (top);
- Assert.Equal (new (0, 0, 80, 25), app.TopRunnableView!.Frame);
- app.Driver!.SetScreenSize (5, 5);
- app.LayoutAndDraw ();
- Assert.Equal (new (0, 0, 5, 5), app.TopRunnableView!.Frame);
-
- if (token is { })
- {
- app.End (token);
- }
- top.Dispose ();
-
- app.Dispose ();
- }
-
- [Fact]
- public void Init_Null_Driver_Should_Pick_A_Driver ()
- {
- IApplication app = Application.Create ();
- app.Init ();
-
- Assert.NotNull (app.Driver);
-
- app.Dispose ();
- }
-
- [Fact]
- public void Init_Dispose_Cleans_Up ()
- {
- IApplication app = Application.Create ();
-
- app.Init ("fake");
-
- app.Dispose ();
-
-#if DEBUG_IDISPOSABLE
- // Validate there are no outstanding Responder-based instances
- // after cleanup
- // Note: We can't check View.Instances in parallel tests as it's a static field
- // that would be shared across parallel test runs
-#endif
- }
-
- [Fact]
- public void Init_Dispose_Fire_InitializedChanged ()
- {
- var initialized = false;
- var Dispose = false;
-
- IApplication app = Application.Create ();
-
- app.InitializedChanged += OnApplicationOnInitializedChanged;
-
- app.Init (driverName: "fake");
- Assert.True (initialized);
- Assert.False (Dispose);
-
- app.Dispose ();
- Assert.True (initialized);
- Assert.True (Dispose);
-
- app.InitializedChanged -= OnApplicationOnInitializedChanged;
-
- return;
-
- void OnApplicationOnInitializedChanged (object? s, EventArgs a)
- {
- if (a.Value)
- {
- initialized = true;
- }
- else
- {
- Dispose = true;
- }
- }
- }
-
- [Fact]
- public void Init_KeyBindings_Are_Not_Reset ()
- {
- IApplication app = Application.Create ();
-
- // Set via Keyboard property (modern API)
- app.Keyboard.QuitKey = Key.Q;
- Assert.Equal (Key.Q, app.Keyboard.QuitKey);
-
- app.Init ("fake");
-
- Assert.Equal (Key.Q, app.Keyboard.QuitKey);
-
- app.Dispose ();
- }
-
- [Fact]
- public void Init_NoParam_ForceDriver_Works ()
- {
- using IApplication app = Application.Create ();
-
- app.ForceDriver = "fake";
- // Note: Init() without params picks up driver configuration
- app.Init ();
-
- Assert.Equal ("fake", app.Driver!.GetName ());
- }
-
- [Fact]
- public void Init_Dispose_Resets_Instance_Properties ()
- {
- IApplication app = Application.Create ();
-
- // Init the app
- app.Init (driverName: "fake");
-
- // Verify initialized
- Assert.True (app.Initialized);
- Assert.NotNull (app.Driver);
-
- // Dispose cleans up
- app.Dispose ();
-
- // Check reset state on the instance
- CheckReset (app);
-
- // Create a new instance and set values
- app = Application.Create ();
- app.Init ("fake");
-
- app.StopAfterFirstIteration = true;
- app.Keyboard.PrevTabGroupKey = Key.A;
- app.Keyboard.NextTabGroupKey = Key.B;
- app.Keyboard.QuitKey = Key.C;
- app.Keyboard.KeyBindings.Add (Key.D, Command.Cancel);
-
- app.Mouse.CachedViewsUnderMouse.Clear ();
- app.Mouse.LastMousePosition = new Point (1, 1);
-
- // Dispose and check reset
- app.Dispose ();
- CheckReset (app);
-
- return;
-
- void CheckReset (IApplication application)
- {
- // Check that all fields and properties are reset on the instance
-
- // Public Properties
- Assert.Null (application.TopRunnableView);
- Assert.Null (application.Mouse.MouseGrabView);
- Assert.Null (application.Driver);
- Assert.False (application.StopAfterFirstIteration);
-
- // Internal properties
- Assert.False (application.Initialized);
- Assert.Null (application.MainThreadId);
- Assert.Empty (application.Mouse.CachedViewsUnderMouse);
- }
- }
-
- [Fact]
- public void Internal_Properties_Correct ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- Assert.True (app.Initialized);
- Assert.Null (app.TopRunnableView);
- SessionToken? rs = app.Begin (new Runnable ());
- Assert.Equal (app.TopRunnable, rs!.Runnable);
- Assert.Null (app.Mouse.MouseGrabView); // public
-
- app.Dispose ();
- }
-
- [Fact]
- public void Invoke_Adds_Idle ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- Runnable top = new ();
- SessionToken? rs = app.Begin (top);
-
- var actionCalled = 0;
- app.Invoke ((_) => { actionCalled++; });
- app.TimedEvents!.RunTimers ();
- Assert.Equal (1, actionCalled);
- top.Dispose ();
-
- app.Dispose ();
- }
-
- [Fact]
- public void Run_Iteration_Fires ()
- {
- var iteration = 0;
-
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- app.Iteration += Application_Iteration;
- app.Run ();
- app.Iteration -= Application_Iteration;
-
- Assert.Equal (1, iteration);
- app.Dispose ();
-
- return;
-
- void Application_Iteration (object? sender, EventArgs e)
- {
-
- iteration++;
- app.RequestStop ();
- }
- }
-
- [Fact]
- public void Screen_Size_Changes ()
- {
- IApplication app = Application.Create ();
- app.Init ("fake");
-
- IDriver? driver = app.Driver;
-
- app.Driver!.SetScreenSize (80, 25);
-
- Assert.Equal (new (0, 0, 80, 25), driver!.Screen);
- Assert.Equal (new (0, 0, 80, 25), app.Screen);
-
- // TODO: Should not be possible to manually change these at whim!
- driver.Cols = 100;
- driver.Rows = 30;
-
- app.Driver!.SetScreenSize (100, 30);
-
- Assert.Equal (new (0, 0, 100, 30), driver.Screen);
-
- app.Screen = new (0, 0, driver.Cols, driver.Rows);
- Assert.Equal (new (0, 0, 100, 30), driver.Screen);
-
- app.Dispose ();
- }
-
- [Fact]
- public void Dispose_Alone_Does_Nothing ()
- {
- IApplication app = Application.Create ();
- app.Dispose ();
- }
-
- #region RunTests
-
- [Fact]
- public void Run_T_After_InitWithDriver_with_Runnable_and_Driver_Does_Not_Throw ()
- {
- IApplication app = Application.Create ();
- app.StopAfterFirstIteration = true;
-
- // Run> when already initialized or not with a Driver will not throw (because Window is derived from Runnable)
- // Using another type not derived from Runnable will throws at compile time
- app.Run (null, "fake");
-
- // Run> when already initialized or not with a Driver will not throw (because Dialog is derived from Runnable)
- app.Run