diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index a14e88d21..e7a3243e2 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -8,7 +8,11 @@ public static partial class Application // Driver abstractions // TODO: Add to IApplication /// Gets the that has been selected. See also . - 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. diff --git a/Terminal.Gui/App/Application.Initialization.cs b/Terminal.Gui/App/Application.Initialization.cs index 1d061209e..188545366 100644 --- a/Terminal.Gui/App/Application.Initialization.cs +++ b/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 - /// Gets of list of types and type names that are available. - /// - [RequiresUnreferencedCode ("AOT")] - public static (List, List) GetDriverTypes () - { - // use reflection to get the list of drivers - List 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 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 - /// - /// Initializes a new instance of a Terminal.Gui Application. must be called when the - /// application is closing. - /// + /// Initializes a new instance of a Terminal.Gui Application. must be called when the application is closing. /// Call this method once per instance (or after has been called). /// /// This function loads the right for the platform, Creates a . 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 drivers, List 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 - /// - /// Gets whether the application has been initialized with and not yet shutdown with - /// . - /// - /// - /// - /// The event is raised after the and - /// methods have been called. - /// - /// - public static bool Initialized { get; internal set; } + internal static int MainThreadId + { + get => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId; + set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value; + } - // TODO: Add to IApplicationLifecycle - /// - /// This event is raised after the and methods have been called. - /// - /// - /// Intended to support unit tests that need to know when the application has been initialized. - /// - public static event EventHandler>? InitializedChanged; - - // TODO: Add to IApplicationLifecycle - /// Shutdown an application initialized with . - /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) - /// and terminal settings are restored. - /// - public static void Shutdown () { ApplicationImpl.Instance.Shutdown (); } - - // 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 drivers, List 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 - /// - /// Raises the event. - /// - internal static void OnInitializedChanged (object sender, EventArgs 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); } + /// Gets of list of types and type names that are available. + /// + [RequiresUnreferencedCode ("AOT")] + public static (List, List) GetDriverTypes () + { + // use reflection to get the list of drivers + List 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 driverTypeNames = driverTypes + .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) + .Select (d => d!.Name) + .Union (["dotnet", "windows", "unix", "fake"]) + .ToList ()!; + + + + return (driverTypes, driverTypeNames); + } + + /// Shutdown an application initialized with . + /// + /// Shutdown must be called for every call to or + /// to ensure all resources are cleaned + /// up (Disposed) + /// and terminal settings are restored. + /// + public static void Shutdown () => ApplicationImpl.Instance.Shutdown (); + + /// + /// Gets whether the application has been initialized with and not yet shutdown with . + /// + /// + /// + /// The event is raised after the and methods have been called. + /// + /// + public static bool Initialized + { + get => ApplicationImpl.Instance.Initialized; + internal set => ApplicationImpl.Instance.Initialized = value; + } + + /// + /// This event is raised after the and methods have been called. + /// + /// + /// Intended to support unit tests that need to know when the application has been initialized. + /// + public static event EventHandler>? InitializedChanged; + + /// + /// Raises the event. + /// + internal static void OnInitializedChanged (object sender, EventArgs e) + { + Application.InitializedChanged?.Invoke (sender, e); + } } diff --git a/Terminal.Gui/App/Application.Navigation.cs b/Terminal.Gui/App/Application.Navigation.cs index 0b35b80c6..28e86d2eb 100644 --- a/Terminal.Gui/App/Application.Navigation.cs +++ b/Terminal.Gui/App/Application.Navigation.cs @@ -7,7 +7,11 @@ public static partial class Application // Navigation stuff /// /// Gets the instance for the current . /// - public static ApplicationNavigation? Navigation { get; internal set; } + public static ApplicationNavigation? Navigation + { + get => ApplicationImpl.Instance.Navigation; + internal set => ApplicationImpl.Instance.Navigation = value; + } /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [ConfigurationProperty (Scope = typeof (SettingsScope))] diff --git a/Terminal.Gui/App/Application.Popover.cs b/Terminal.Gui/App/Application.Popover.cs index 25a8ebe9c..31522f80f 100644 --- a/Terminal.Gui/App/Application.Popover.cs +++ b/Terminal.Gui/App/Application.Popover.cs @@ -5,5 +5,9 @@ namespace Terminal.Gui.App; public static partial class Application // Popover handling { /// Gets the Application manager. - public static ApplicationPopover? Popover { get; internal set; } + public static ApplicationPopover? Popover + { + get => ApplicationImpl.Instance.Popover; + internal set => ApplicationImpl.Instance.Popover = value; + } } \ No newline at end of file diff --git a/Terminal.Gui/App/Application.Toplevel.cs b/Terminal.Gui/App/Application.Toplevel.cs index add62a5a5..cea9818ef 100644 --- a/Terminal.Gui/App/Application.Toplevel.cs +++ b/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 _topLevels = new (); - private static readonly object _topLevelsLock = new (); - /// Holds the stack of TopLevel views. - internal static ConcurrentStack TopLevels - { - get - { - lock (_topLevelsLock) - { - return _topLevels; - } - } - } + internal static ConcurrentStack TopLevels => ApplicationImpl.Instance.TopLevels; /// The that is currently active. /// The top. - public static Toplevel? Top { get; internal set; } + public static Toplevel? Top + { + get => ApplicationImpl.Instance.Top; + internal set => ApplicationImpl.Instance.Top = value; + } } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 7d2b69135..5668d9450 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/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; /// -/// Implementation of core methods using the modern -/// main loop architecture with component factories for different platforms. +/// Implementation of core methods using the modern +/// main loop architecture with component factories for different platforms. /// 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 _topLevels = new (); + private int _mainThreadId = -1; + // Private static readonly Lazy instance of Application private static Lazy _lazyInstance = new (() => new ApplicationImpl ()); /// - /// Creates a new instance of the Application backend. + /// Gets the currently configured backend implementation of gateway methods. + /// Change to your own implementation by using (before init). /// - 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; /// public ITimedEvents? TimedEvents => _timedEvents; + internal IMainLoopCoordinator? Coordinator => _coordinator; + private IMouse? _mouse; /// @@ -50,6 +57,11 @@ public class ApplicationImpl : IApplication set => _mouse = value ?? throw new ArgumentNullException (nameof (value)); } + /// + /// Handles which (if any) has captured the mouse + /// + public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler (); + private IKeyboard? _keyboard; /// @@ -71,74 +83,74 @@ public class ApplicationImpl : IApplication /// public IConsoleDriver? Driver { - get => Application.Driver; - set => Application.Driver = value; + get => _driver; + set => _driver = value; } /// public bool Initialized { - get => Application.Initialized; - set => Application.Initialized = value; + get => _initialized; + set => _initialized = value; } /// public ApplicationPopover? Popover { - get => Application.Popover; - set => Application.Popover = value; + get => _popover; + set => _popover = value; } /// 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 /// public Toplevel? Top { - get => Application.Top; - set => Application.Top = value; + get => _top; + set => _top = value; } /// - public ConcurrentStack TopLevels => Application.TopLevels; + public ConcurrentStack TopLevels => _topLevels; - /// - public void LayoutAndDraw (bool forceRedraw = false) + /// + /// Gets or sets the main thread ID for the application. + /// + internal int MainThreadId { - List tops = [.. TopLevels]; + get => _mainThreadId; + set => _mainThreadId = value; + } - if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) - { - visiblePopover.SetNeedsDraw (); - visiblePopover.SetNeedsLayout (); - tops.Insert (0, visiblePopover); - } + /// + public void RequestStop () => RequestStop (null); - // BUGBUG: Application.Screen needs to be moved to IApplication - bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size); + /// + /// Creates a new instance of the Application backend. + /// + public ApplicationImpl () + { + } - // 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; - } + internal ApplicationImpl (IComponentFactory componentFactory) + { + _componentFactory = componentFactory; + } - if (forceRedraw) - { - Driver?.ClearContents (); - } - - View.SetClipToScreen (); - View.Draw (tops, neededLayout || forceRedraw); - View.SetClipToScreen (); - Driver?.Refresh (); + /// + /// 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 . + /// + /// + public static void ChangeInstance (IApplication newApplication) + { + _lazyInstance = new Lazy (newApplication); } /// @@ -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; + bool factoryIsDotNet = _componentFactory is IComponentFactory; + bool factoryIsUnix = _componentFactory is IComponentFactory; + bool factoryIsFake = _componentFactory is IComponentFactory; + + // 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 (Func> fallbackFactory) + { + ConcurrentQueue inputBuffer = new (); + ApplicationMainLoop loop = new (); + + IComponentFactory cf; + + if (_componentFactory is IComponentFactory typedFactory) + { + cf = typedFactory; + } + else + { + cf = fallbackFactory (); + } + + return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); } /// @@ -228,15 +325,14 @@ public class ApplicationImpl : IApplication public T Run (Func? 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); } /// Shutdown an application initialized with . 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 ()); } - /// + /// 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; } - /// - public void RequestStop () => Application.RequestStop (); - - - /// + /// 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; + } + ); } - /// + /// public bool IsLegacy => false; - /// + /// public object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } - /// + /// public bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } - /// - /// 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 . - /// - /// - public static void ChangeInstance (IApplication newApplication) { _lazyInstance = new (newApplication); } - - /// - /// Gets the currently configured backend implementation of gateway methods. - /// Change to your own implementation by using (before init). - /// - public static IApplication Instance => _lazyInstance.Value; - - internal IMainLoopCoordinator? Coordinator { get; private set; } - - private void CreateDriver (string? driverName) + /// + 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) + List tops = [.. _topLevels]; + + if (_popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { - 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; + visiblePopover.SetNeedsDraw (); + visiblePopover.SetNeedsLayout (); + tops.Insert (0, visiblePopover); } - PlatformID p = Environment.OSVersion.Platform; + // BUGBUG: Application.Screen needs to be moved to IApplication + bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Application.Screen.Size); - // Check component factory type first - this takes precedence over driverName - bool factoryIsWindows = _componentFactory is IComponentFactory; - bool factoryIsDotNet = _componentFactory is IComponentFactory; - bool factoryIsUnix = _componentFactory is IComponentFactory; - bool factoryIsFake = _componentFactory is IComponentFactory; - - // 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)) + // BUGBUG: Application.ClearScreenNextIteration needs to be moved to IApplication + if (Application.ClearScreenNextIteration) { - 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 ()); + forceRedraw = true; + // BUGBUG: Application.Screen needs to be moved to IApplication + Application.ClearScreenNextIteration = false; } - Coordinator.StartAsync ().Wait (); - - if (Application.Driver == null) + if (forceRedraw) { - throw new ("Application.Driver was null even after booting MainLoopCoordinator"); - } - } - - private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) - { - ConcurrentQueue inputBuffer = new (); - ApplicationMainLoop loop = new (); - - IComponentFactory cf; - - if (_componentFactory is IComponentFactory typedFactory) - { - cf = typedFactory; - } - else - { - cf = fallbackFactory (); + _driver?.ClearContents (); } - return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); + View.SetClipToScreen (); + View.Draw (tops, neededLayout || forceRedraw); + View.SetClipToScreen (); + _driver?.Refresh (); } } diff --git a/Tests/UnitTests/Application/ApplicationImplTests.cs b/Tests/UnitTests/Application/ApplicationImplTests.cs index 7b9d81f53..c04b6dfad 100644 --- a/Tests/UnitTests/Application/ApplicationImplTests.cs +++ b/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); + } }