Browse Source

Fixes #4125 - Decouple ApplicationImpl from static Application references (#4324)

* Initial plan

* Add instance fields to ApplicationImpl and update static Application properties to delegate to ApplicationImpl.Instance

Co-authored-by: tig <[email protected]>

* Fix code review issues: use instance fields in Run() and improve Shutdown() logic

Co-authored-by: tig <[email protected]>

* Final code review fix: use _initialized directly in Shutdown

Co-authored-by: tig <[email protected]>

* Fix Shutdown order: call ResetState before clearing instance fields

Co-authored-by: tig <[email protected]>

* Add MainThreadId as instance field in ApplicationImpl

Co-authored-by: tig <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: tig <[email protected]>
Co-authored-by: Tig <[email protected]>
Copilot 1 month ago
parent
commit
5199663551

+ 5 - 1
Terminal.Gui/App/Application.Driver.cs

@@ -8,7 +8,11 @@ public static partial class Application // Driver abstractions
 
     // TODO: Add to IApplication
     /// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary>
-    public static IConsoleDriver? Driver { get; internal set; }
+    public static IConsoleDriver? Driver
+    {
+        get => ApplicationImpl.Instance.Driver;
+        internal set => ApplicationImpl.Instance.Driver = value;
+    }
 
     // TODO: Add to IApplication
     // BUGBUG: Force16Colors should be nullable.

+ 83 - 84
Terminal.Gui/App/Application.Initialization.cs

@@ -5,42 +5,10 @@ using System.Reflection;
 
 namespace Terminal.Gui.App;
 
-public static partial class Application // Lifecycle (Init/Shutdown)
+public static partial class Application // Initialization (Init/Shutdown)
 {
-    // TODO: Add to IApplication
-    /// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
-    /// <returns></returns>
-    [RequiresUnreferencedCode ("AOT")]
-    public static (List<Type?>, List<string?>) GetDriverTypes ()
-    {
-        // use reflection to get the list of drivers
-        List<Type?> driverTypes = new ();
-
-        // Only inspect the IConsoleDriver assembly
-        Assembly asm = typeof (IConsoleDriver).Assembly;
-
-        foreach (Type? type in asm.GetTypes ())
-        {
-            if (typeof (IConsoleDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
-            {
-                driverTypes.Add (type);
-            }
-        }
-
-        List<string?> driverTypeNames = driverTypes
-                                        .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
-                                        .Select (d => d!.Name)
-                                        .Union (["dotnet", "windows", "unix", "fake"])
-                                        .ToList ()!;
 
-        return (driverTypes, driverTypeNames);
-    }
-
-    // TODO: Add to IApplicationLifecycle
-    /// <summary>
-    ///     Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the
-    ///     application is closing.
-    /// </summary>
+    /// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
     /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
     /// <para>
     ///     This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and
@@ -76,60 +44,31 @@ public static partial class Application // Lifecycle (Init/Shutdown)
         // that isn't supported by the modern application architecture
         if (driver is null)
         {
-            string driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
-
+            var driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
             if (!string.IsNullOrEmpty (driverNameToCheck))
             {
                 (List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
                 Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (driverNameToCheck, StringComparison.InvariantCultureIgnoreCase));
-
+                
                 // If it's a legacy IConsoleDriver (not a Facade), use InternalInit which supports legacy drivers
                 if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
                 {
                     InternalInit (driver, driverName);
-
                     return;
                 }
             }
         }
-
+        
         // Otherwise delegate to the ApplicationImpl instance (which uses the modern architecture)
         ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
     }
 
-    // TODO: Add to IApplicationLifecycle
-    /// <summary>
-    ///     Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with
-    ///     <see cref="Shutdown"/>.
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/>
-    ///         methods have been called.
-    ///     </para>
-    /// </remarks>
-    public static bool Initialized { get; internal set; }
-
-    // TODO: Add to IApplicationLifecycle
-    /// <summary>
-    ///     This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
-    /// </summary>
-    /// <remarks>
-    ///     Intended to support unit tests that need to know when the application has been initialized.
-    /// </remarks>
-    public static event EventHandler<EventArgs<bool>>? InitializedChanged;
-
-    // TODO: Add to IApplicationLifecycle
-    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
-    /// <remarks>
-    ///     Shutdown must be called for every call to <see cref="Init"/> or
-    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
-    ///     up (Disposed)
-    ///     and terminal settings are restored.
-    /// </remarks>
-    public static void Shutdown () { ApplicationImpl.Instance.Shutdown (); }
+    internal static int MainThreadId
+    {
+        get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId;
+        set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
+    }
 
-    // TODO: Add to IApplicationLifecycle
     // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
     //
     // Called from:
@@ -160,7 +99,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
         if (!calledViaRunT)
         {
             // Reset all class variables (Application is a singleton).
-            ResetState (true);
+            ResetState (ignoreDisposed: true);
         }
 
         // For UnitTests
@@ -185,7 +124,7 @@ public static partial class Application // Lifecycle (Init/Shutdown)
             //{
             //    (List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
             //    Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (ForceDriver, StringComparison.InvariantCultureIgnoreCase));
-
+                
             //    if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
             //    {
             //        // This is a legacy driver (not a ConsoleDriverFacade)
@@ -193,13 +132,12 @@ public static partial class Application // Lifecycle (Init/Shutdown)
             //        useLegacyDriver = true;
             //    }
             //}
-
+            
             //// Use the modern application architecture
             //if (!useLegacyDriver)
             {
                 ApplicationImpl.Instance.Init (driver, driverName);
                 Debug.Assert (Driver is { });
-
                 return;
             }
         }
@@ -240,14 +178,6 @@ public static partial class Application // Lifecycle (Init/Shutdown)
         InitializedChanged?.Invoke (null, new (init));
     }
 
-    internal static int MainThreadId { get; set; } = -1;
-
-    // TODO: Add to IApplicationLifecycle
-    /// <summary>
-    ///     Raises the <see cref="InitializedChanged"/> event.
-    /// </summary>
-    internal static void OnInitializedChanged (object sender, EventArgs<bool> e) { InitializedChanged?.Invoke (sender, e); }
-
     internal static void SubscribeDriverEvents ()
     {
         ArgumentNullException.ThrowIfNull (Driver);
@@ -268,9 +198,78 @@ public static partial class Application // Lifecycle (Init/Shutdown)
         Driver.MouseEvent -= Driver_MouseEvent;
     }
 
+    private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
     private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
     private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
     private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
 
-    private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
+    /// <summary>Gets of list of <see cref="IConsoleDriver"/> types and type names that are available.</summary>
+    /// <returns></returns>
+    [RequiresUnreferencedCode ("AOT")]
+    public static (List<Type?>, List<string?>) GetDriverTypes ()
+    {
+        // use reflection to get the list of drivers
+        List<Type?> driverTypes = new ();
+
+        // Only inspect the IConsoleDriver assembly
+        var asm = typeof (IConsoleDriver).Assembly;
+
+        foreach (Type? type in asm.GetTypes ())
+        {
+            if (typeof (IConsoleDriver).IsAssignableFrom (type) &&
+                type is { IsAbstract: false, IsClass: true })
+            {
+                driverTypes.Add (type);
+            }
+        }
+
+        List<string?> driverTypeNames = driverTypes
+                                        .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
+                                        .Select (d => d!.Name)
+                                        .Union (["dotnet", "windows", "unix", "fake"])
+                                        .ToList ()!;
+
+
+
+        return (driverTypes, driverTypeNames);
+    }
+
+    /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
+    /// <remarks>
+    ///     Shutdown must be called for every call to <see cref="Init"/> or
+    ///     <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned
+    ///     up (Disposed)
+    ///     and terminal settings are restored.
+    /// </remarks>
+    public static void Shutdown () => ApplicationImpl.Instance.Shutdown ();
+
+    /// <summary>
+    ///     Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    ///     The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
+    /// </para>
+    /// </remarks>
+    public static bool Initialized
+    {
+        get => ApplicationImpl.Instance.Initialized;
+        internal set => ApplicationImpl.Instance.Initialized = value;
+    }
+
+    /// <summary>
+    ///     This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
+    /// </summary>
+    /// <remarks>
+    ///     Intended to support unit tests that need to know when the application has been initialized.
+    /// </remarks>
+    public static event EventHandler<EventArgs<bool>>? InitializedChanged;
+
+    /// <summary>
+    ///  Raises the <see cref="InitializedChanged"/> event.
+    /// </summary>
+    internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
+    {
+        Application.InitializedChanged?.Invoke (sender, e);
+    }
 }

+ 5 - 1
Terminal.Gui/App/Application.Navigation.cs

@@ -7,7 +7,11 @@ public static partial class Application // Navigation stuff
     /// <summary>
     ///     Gets the <see cref="ApplicationNavigation"/> instance for the current <see cref="Application"/>.
     /// </summary>
-    public static ApplicationNavigation? Navigation { get; internal set; }
+    public static ApplicationNavigation? Navigation
+    {
+        get => ApplicationImpl.Instance.Navigation;
+        internal set => ApplicationImpl.Instance.Navigation = value;
+    }
 
     /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]

+ 5 - 1
Terminal.Gui/App/Application.Popover.cs

@@ -5,5 +5,9 @@ namespace Terminal.Gui.App;
 public static partial class Application // Popover handling
 {
     /// <summary>Gets the Application <see cref="Popover"/> manager.</summary>
-    public static ApplicationPopover? Popover { get; internal set; }
+    public static ApplicationPopover? Popover
+    {
+        get => ApplicationImpl.Instance.Popover;
+        internal set => ApplicationImpl.Instance.Popover = value;
+    }
 }

+ 6 - 14
Terminal.Gui/App/Application.Toplevel.cs

@@ -7,22 +7,14 @@ public static partial class Application // Toplevel handling
 {
     // BUGBUG: Technically, this is not the full lst of TopLevels. There be dragons here, e.g. see how Toplevel.Id is used. What
 
-    private static readonly ConcurrentStack<Toplevel> _topLevels = new ();
-    private static readonly object _topLevelsLock = new ();
-
     /// <summary>Holds the stack of TopLevel views.</summary>
-    internal static ConcurrentStack<Toplevel> TopLevels
-    {
-        get
-        {
-            lock (_topLevelsLock)
-            {
-                return _topLevels;
-            }
-        }
-    }
+    internal static ConcurrentStack<Toplevel> TopLevels => ApplicationImpl.Instance.TopLevels;
 
     /// <summary>The <see cref="Toplevel"/> that is currently active.</summary>
     /// <value>The top.</value>
-    public static Toplevel? Top { get; internal set; }
+    public static Toplevel? Top
+    {
+        get => ApplicationImpl.Instance.Top;
+        internal set => ApplicationImpl.Instance.Top = value;
+    }
 }

+ 221 - 186
Terminal.Gui/App/ApplicationImpl.cs

@@ -3,35 +3,42 @@ using System.Collections.Concurrent;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Logging;
+using Terminal.Gui.Drivers;
 
 namespace Terminal.Gui.App;
 
 /// <summary>
-///     Implementation of core <see cref="Application"/> methods using the modern
-///     main loop architecture with component factories for different platforms.
+/// Implementation of core <see cref="Application"/> methods using the modern
+/// main loop architecture with component factories for different platforms.
 /// </summary>
 public class ApplicationImpl : IApplication
 {
+    private readonly IComponentFactory? _componentFactory;
+    private IMainLoopCoordinator? _coordinator;
+    private string? _driverName;
+    private readonly ITimedEvents _timedEvents = new TimedEvents ();
+    private IConsoleDriver? _driver;
+    private bool _initialized;
+    private ApplicationPopover? _popover;
+    private ApplicationNavigation? _navigation;
+    private Toplevel? _top;
+    private readonly ConcurrentStack<Toplevel> _topLevels = new ();
+    private int _mainThreadId = -1;
+
     // Private static readonly Lazy instance of Application
     private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ());
 
     /// <summary>
-    ///     Creates a new instance of the Application backend.
+    /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
+    /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
     /// </summary>
-    public ApplicationImpl () { }
-
-    internal ApplicationImpl (IComponentFactory componentFactory)
-    {
-        _componentFactory = componentFactory;
-    }
-
-    private readonly IComponentFactory? _componentFactory;
-    private readonly ITimedEvents _timedEvents = new TimedEvents ();
-    private string? _driverName;
+    public static IApplication Instance => _lazyInstance.Value;
 
     /// <inheritdoc/>
     public ITimedEvents? TimedEvents => _timedEvents;
 
+    internal IMainLoopCoordinator? Coordinator => _coordinator;
+
     private IMouse? _mouse;
 
     /// <summary>
@@ -50,6 +57,11 @@ public class ApplicationImpl : IApplication
         set => _mouse = value ?? throw new ArgumentNullException (nameof (value));
     }
 
+    /// <summary>
+    /// Handles which <see cref="View"/> (if any) has captured the mouse
+    /// </summary>
+    public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
+
     private IKeyboard? _keyboard;
 
     /// <summary>
@@ -71,74 +83,74 @@ public class ApplicationImpl : IApplication
     /// <inheritdoc/>
     public IConsoleDriver? Driver
     {
-        get => Application.Driver;
-        set => Application.Driver = value;
+        get => _driver;
+        set => _driver = value;
     }
 
     /// <inheritdoc/>
     public bool Initialized
     {
-        get => Application.Initialized;
-        set => Application.Initialized = value;
+        get => _initialized;
+        set => _initialized = value;
     }
 
     /// <inheritdoc/>
     public ApplicationPopover? Popover
     {
-        get => Application.Popover;
-        set => Application.Popover = value;
+        get => _popover;
+        set => _popover = value;
     }
 
     /// <inheritdoc/>
     public ApplicationNavigation? Navigation
     {
-        get => Application.Navigation;
-        set => Application.Navigation = value;
+        get => _navigation;
+        set => _navigation = value;
     }
 
-    // TODO: Create an IViewHierarchy that encapsulates Top and TopLevels and LayoutAndDraw
     /// <inheritdoc/>
     public Toplevel? Top
     {
-        get => Application.Top;
-        set => Application.Top = value;
+        get => _top;
+        set => _top = value;
     }
 
     /// <inheritdoc/>
-    public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
+    public ConcurrentStack<Toplevel> TopLevels => _topLevels;
 
-    /// <inheritdoc />
-    public void LayoutAndDraw (bool forceRedraw = false)
+    /// <summary>
+    /// Gets or sets the main thread ID for the application.
+    /// </summary>
+    internal int MainThreadId
     {
-        List<View> tops = [.. TopLevels];
-
-        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
-        {
-            visiblePopover.SetNeedsDraw ();
-            visiblePopover.SetNeedsLayout ();
-            tops.Insert (0, visiblePopover);
-        }
+        get => _mainThreadId;
+        set => _mainThreadId = value;
+    }
 
-        // BUGBUG: Application.Screen needs to be moved to IApplication
-        bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
+    /// <inheritdoc/>
+    public void RequestStop () => RequestStop (null);
 
-        // BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
-        if (Application.ClearScreenNextIteration)
-        {
-            forceRedraw = true;
-            // BUGBUG: Application.Screen needs to be moved to IApplication
-            Application.ClearScreenNextIteration = false;
-        }
+    /// <summary>
+    /// Creates a new instance of the Application backend.
+    /// </summary>
+    public ApplicationImpl ()
+    {
+    }
 
-        if (forceRedraw)
-        {
-            Driver?.ClearContents ();
-        }
+    internal ApplicationImpl (IComponentFactory componentFactory)
+    {
+        _componentFactory = componentFactory;
+    }
 
-        View.SetClipToScreen ();
-        View.Draw (tops, neededLayout || forceRedraw);
-        View.SetClipToScreen ();
-        Driver?.Refresh ();
+    /// <summary>
+    /// Change the singleton implementation, should not be called except before application
+    /// startup. This method lets you provide alternative implementations of core static gateway
+    /// methods of <see cref="Application"/>.
+    /// </summary>
+    /// <param name="newApplication"></param>
+    public static void ChangeInstance (IApplication newApplication)
+    {
+        _lazyInstance = new Lazy<IApplication> (newApplication);
     }
 
     /// <inheritdoc/>
@@ -146,7 +158,7 @@ public class ApplicationImpl : IApplication
     [RequiresDynamicCode ("AOT")]
     public void Init (IConsoleDriver? driver = null, string? driverName = null)
     {
-        if (Application.Initialized)
+        if (_initialized)
         {
             Logging.Logger.LogError ("Init called multiple times without shutdown, aborting.");
 
@@ -163,13 +175,12 @@ public class ApplicationImpl : IApplication
             _driverName = Application.ForceDriver;
         }
 
-        Debug.Assert (Application.Navigation is null);
-        Application.Navigation = new ();
+        Debug.Assert(_navigation is null);
+        _navigation = new ();
 
-        Debug.Assert (Application.Popover is null);
-        Application.Popover = new ();
+        Debug.Assert (_popover is null);
+        _popover = new ();
 
-        // TODO: Move this into IKeyboard and Keyboard implementation
         // Preserve existing keyboard settings if they exist
         bool hasExistingKeyboard = _keyboard is not null;
         Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
@@ -195,13 +206,99 @@ public class ApplicationImpl : IApplication
 
         CreateDriver (driverName ?? _driverName);
 
-        Application.Initialized = true;
+        _initialized = true;
 
         Application.OnInitializedChanged (this, new (true));
         Application.SubscribeDriverEvents ();
 
         SynchronizationContext.SetSynchronizationContext (new ());
-        Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
+        _mainThreadId = Thread.CurrentThread.ManagedThreadId;
+    }
+
+    private void CreateDriver (string? driverName)
+    {
+        // When running unit tests, always use FakeDriver unless explicitly specified
+        if (ConsoleDriver.RunningUnitTests && 
+            string.IsNullOrEmpty (driverName) && 
+            _componentFactory is null)
+        {
+            Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
+            _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
+            _coordinator.StartAsync ().Wait ();
+
+            if (_driver == null)
+            {
+                throw new ("Driver was null even after booting MainLoopCoordinator");
+            }
+
+            return;
+        }
+
+        PlatformID p = Environment.OSVersion.Platform;
+
+        // Check component factory type first - this takes precedence over driverName
+        bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
+        bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+        bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
+        bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+
+        // Then check driverName
+        bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsDotNet = (driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false);
+        bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
+        bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
+
+        // Decide which driver to use - component factory type takes priority
+        if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
+        {
+            _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
+        }
+        else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
+        {
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
+        }
+        else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
+        {
+            _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
+        }
+        else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
+        {
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
+        }
+        else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+        {
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
+        }
+        else
+        {
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
+        }
+
+        _coordinator.StartAsync ().Wait ();
+
+        if (_driver == null)
+        {
+            throw new ("Driver was null even after booting MainLoopCoordinator");
+        }
+    }
+
+    private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
+    {
+        ConcurrentQueue<T> inputBuffer = new ();
+        ApplicationMainLoop<T> loop = new ();
+
+        IComponentFactory<T> cf;
+
+        if (_componentFactory is IComponentFactory<T> typedFactory)
+        {
+            cf = typedFactory;
+        }
+        else
+        {
+            cf = fallbackFactory ();
+        }
+
+        return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
     }
 
     /// <summary>
@@ -228,15 +325,14 @@ public class ApplicationImpl : IApplication
     public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
         where T : Toplevel, new()
     {
-        if (!Application.Initialized)
+        if (!_initialized)
         {
             // Init() has NOT been called. Auto-initialize as per interface contract.
-            Init (driver);
+            Init (driver, null);
         }
 
         T top = new ();
         Run (top, errorHandler);
-
         return top;
     }
 
@@ -248,62 +344,74 @@ public class ApplicationImpl : IApplication
         Logging.Information ($"Run '{view}'");
         ArgumentNullException.ThrowIfNull (view);
 
-        if (!Application.Initialized)
+        if (!_initialized)
         {
             throw new NotInitializedException (nameof (Run));
         }
 
-        if (Application.Driver == null)
+        if (_driver == null)
         {
-            throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
+            throw new  InvalidOperationException ("Driver was inexplicably null when trying to Run view");
         }
 
-        Application.Top = view;
+        _top = view;
 
         RunState rs = Application.Begin (view);
 
-        Application.Top.Running = true;
+        _top.Running = true;
 
-        while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
+        while (_topLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
         {
-            if (Coordinator is null)
+            if (_coordinator is null)
             {
                 throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run");
             }
 
-            Coordinator.RunIteration ();
+            _coordinator.RunIteration ();
         }
 
-        Logging.Information ("Run - Calling End");
+        Logging.Information ($"Run - Calling End");
         Application.End (rs);
     }
 
     /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary>
     public void Shutdown ()
     {
-        Coordinator?.Stop ();
-
-        bool wasInitialized = Application.Initialized;
+        _coordinator?.Stop ();
+        
+        bool wasInitialized = _initialized;
+        
+        // Call ResetState FIRST so it can properly dispose Popover and other resources
+        // that are accessed via Application.* static properties that now delegate to instance fields
         Application.ResetState ();
         ConfigurationManager.PrintJsonErrors ();
+        
+        // Clear instance fields after ResetState has disposed everything
+        _driver = null;
+        _mouse = null;
+        _keyboard = null;
+        _initialized = false;
+        _navigation = null;
+        _popover = null;
+        _top = null;
+        _topLevels.Clear ();
+        _mainThreadId = -1;
 
         if (wasInitialized)
         {
-            bool init = Application.Initialized;
+            bool init = _initialized; // Will be false after clearing fields above
             Application.OnInitializedChanged (this, new (in init));
         }
 
-        Application.Driver = null;
-        _keyboard = null;
         _lazyInstance = new (() => new ApplicationImpl ());
     }
 
-    /// <inheritdoc/>
+    /// <inheritdoc />
     public void RequestStop (Toplevel? top)
     {
-        Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");
+        Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
 
-        top ??= Application.Top;
+        top ??= _top;
 
         if (top == null)
         {
@@ -321,138 +429,65 @@ public class ApplicationImpl : IApplication
         top.Running = false;
     }
 
-    /// <inheritdoc/>
-    public void RequestStop () => Application.RequestStop ();
-
-
-    /// <inheritdoc/>
+    /// <inheritdoc />
     public void Invoke (Action action)
     {
         // If we are already on the main UI thread
-        if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId)
+        if (_mainThreadId == Thread.CurrentThread.ManagedThreadId)
         {
             action ();
-
             return;
         }
 
-        _timedEvents.Add (
-                          TimeSpan.Zero,
-                          () =>
-                          {
-                              action ();
-
-                              return false;
-                          }
-                         );
+        _timedEvents.Add (TimeSpan.Zero,
+                              () =>
+                              {
+                                  action ();
+                                  return false;
+                              }
+                             );
     }
 
-    /// <inheritdoc/>
+    /// <inheritdoc />
     public bool IsLegacy => false;
 
-    /// <inheritdoc/>
+    /// <inheritdoc />
     public object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.Add (time, callback); }
 
-    /// <inheritdoc/>
+    /// <inheritdoc />
     public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); }
 
-    /// <summary>
-    ///     Change the singleton implementation, should not be called except before application
-    ///     startup. This method lets you provide alternative implementations of core static gateway
-    ///     methods of <see cref="Application"/>.
-    /// </summary>
-    /// <param name="newApplication"></param>
-    public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); }
-
-    /// <summary>
-    ///     Gets the currently configured backend implementation of <see cref="Application"/> gateway methods.
-    ///     Change to your own implementation by using <see cref="ChangeInstance"/> (before init).
-    /// </summary>
-    public static IApplication Instance => _lazyInstance.Value;
-
-    internal IMainLoopCoordinator? Coordinator { get; private set; }
-
-    private void CreateDriver (string? driverName)
+    /// <inheritdoc />
+    public void LayoutAndDraw (bool forceRedraw = false)
     {
-        // When running unit tests, always use FakeDriver unless explicitly specified
-        if (ConsoleDriver.RunningUnitTests && string.IsNullOrEmpty (driverName) && _componentFactory is null)
-        {
-            Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
-            Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
-            Coordinator.StartAsync ().Wait ();
-
-            if (Application.Driver == null)
-            {
-                throw new ("Application.Driver was null even after booting MainLoopCoordinator");
-            }
-
-            return;
-        }
-
-        PlatformID p = Environment.OSVersion.Platform;
-
-        // Check component factory type first - this takes precedence over driverName
-        bool factoryIsWindows = _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
-        bool factoryIsDotNet = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
-        bool factoryIsUnix = _componentFactory is IComponentFactory<char>;
-        bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
-
-        // Then check driverName
-        bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
-        bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
-        bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
-        bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;
+        List<View> tops = [.. _topLevels];
 
-        // Decide which driver to use - component factory type takes priority
-        if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
-        {
-            Coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
-        }
-        else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
-        {
-            Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
-        }
-        else if (factoryIsDotNet || (!factoryIsWindows && !factoryIsUnix && nameIsDotNet))
-        {
-            Coordinator = CreateSubcomponents (() => new NetComponentFactory ());
-        }
-        else if (factoryIsUnix || (!factoryIsWindows && !factoryIsDotNet && nameIsUnix))
+        if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
         {
-            Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
-        }
-        else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
-        {
-            Coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
-        }
-        else
-        {
-            Coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
+            visiblePopover.SetNeedsDraw ();
+            visiblePopover.SetNeedsLayout ();
+            tops.Insert (0, visiblePopover);
         }
 
-        Coordinator.StartAsync ().Wait ();
+        // BUGBUG: Application.Screen needs to be moved to IApplication
+        bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size);
 
-        if (Application.Driver == null)
+        // BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication
+        if (Application.ClearScreenNextIteration)
         {
-            throw new ("Application.Driver was null even after booting MainLoopCoordinator");
+            forceRedraw = true;
+            // BUGBUG: Application.Screen needs to be moved to IApplication
+            Application.ClearScreenNextIteration = false;
         }
-    }
-
-    private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
-    {
-        ConcurrentQueue<T> inputBuffer = new ();
-        ApplicationMainLoop<T> loop = new ();
-
-        IComponentFactory<T> cf;
 
-        if (_componentFactory is IComponentFactory<T> typedFactory)
-        {
-            cf = typedFactory;
-        }
-        else
+        if (forceRedraw)
         {
-            cf = fallbackFactory ();
+            _driver?.ClearContents ();
         }
 
-        return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
+        View.SetClipToScreen ();
+        View.Draw (tops, neededLayout || forceRedraw);
+        View.SetClipToScreen ();
+        _driver?.Refresh ();
     }
 }

+ 48 - 0
Tests/UnitTests/Application/ApplicationImplTests.cs

@@ -583,4 +583,52 @@ public class ApplicationImplTests
 
         Assert.True (result);
     }
+
+    [Fact]
+    public void ApplicationImpl_UsesInstanceFields_NotStaticReferences()
+    {
+        // This test verifies that ApplicationImpl uses instance fields instead of static Application references
+        var orig = ApplicationImpl.Instance;
+
+        var v2 = NewApplicationImpl();
+        ApplicationImpl.ChangeInstance(v2);
+
+        // Before Init, all fields should be null/default
+        Assert.Null(v2.Driver);
+        Assert.False(v2.Initialized);
+        Assert.Null(v2.Popover);
+        Assert.Null(v2.Navigation);
+        Assert.Null(v2.Top);
+        Assert.Empty(v2.TopLevels);
+
+        // Init should populate instance fields
+        v2.Init();
+
+        // After Init, Driver, Navigation, and Popover should be populated
+        Assert.NotNull(v2.Driver);
+        Assert.True(v2.Initialized);
+        Assert.NotNull(v2.Popover);
+        Assert.NotNull(v2.Navigation);
+        Assert.Null(v2.Top); // Top is still null until Run
+
+        // Verify that static Application properties delegate to instance
+        Assert.Equal(v2.Driver, Application.Driver);
+        Assert.Equal(v2.Initialized, Application.Initialized);
+        Assert.Equal(v2.Popover, Application.Popover);
+        Assert.Equal(v2.Navigation, Application.Navigation);
+        Assert.Equal(v2.Top, Application.Top);
+        Assert.Same(v2.TopLevels, Application.TopLevels);
+
+        // Shutdown should clean up instance fields
+        v2.Shutdown();
+
+        Assert.Null(v2.Driver);
+        Assert.False(v2.Initialized);
+        Assert.Null(v2.Popover);
+        Assert.Null(v2.Navigation);
+        Assert.Null(v2.Top);
+        Assert.Empty(v2.TopLevels);
+
+        ApplicationImpl.ChangeInstance(orig);
+    }
 }