#nullable enable using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; /// /// Implementation of that boots the new 'v2' /// main loop architecture. /// public class ApplicationV2 : ApplicationImpl { private readonly IComponentFactory? _componentFactory; private IMainLoopCoordinator? _coordinator; private string? _driverName; private readonly ITimedEvents _timedEvents = new TimedEvents (); /// public override ITimedEvents TimedEvents => _timedEvents; internal IMainLoopCoordinator? Coordinator => _coordinator; /// /// Creates anew instance of the Application backend. The provided /// factory methods will be used on Init calls to get things booted. /// public ApplicationV2 () { IsLegacy = false; } internal ApplicationV2 (IComponentFactory componentFactory) { _componentFactory = componentFactory; IsLegacy = false; } /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public override void Init (IConsoleDriver? driver = null, string? driverName = null) { if (Application.Initialized) { Logging.Logger.LogError ("Init called multiple times without shutdown, ignoring."); return; } if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; } Debug.Assert(Application.Navigation is null); Application.Navigation = new (); Debug.Assert (Application.Popover is null); Application.Popover = new (); Application.AddKeyBindings (); // This is consistent with Application.ForceDriver which magnetically picks up driverName // making it use custom driver in future shutdown/init calls where no driver is specified CreateDriver (driverName ?? _driverName); Application.Initialized = true; Application.OnInitializedChanged (this, new (true)); Application.SubscribeDriverEvents (); SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); Application.MainThreadId = Thread.CurrentThread.ManagedThreadId; } private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; bool definetlyWin = (driverName?.Contains ("win") ?? false) || _componentFactory is IComponentFactory; bool definetlyNet = (driverName?.Contains ("net") ?? false) || _componentFactory is IComponentFactory; bool definetlyUnix = (driverName?.Contains ("unix") ?? false) || _componentFactory is IComponentFactory; if (definetlyWin) { _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ()); } else if (definetlyNet) { _coordinator = CreateSubcomponents (() => new NetComponentFactory ()); } else if (definetlyUnix) { _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 (Application.Driver == null) { throw new ("Application.Driver was null even after booting MainLoopCoordinator"); } } private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) { ConcurrentQueue inputBuffer = new (); MainLoop loop = new (); IComponentFactory cf; if (_componentFactory is IComponentFactory typedFactory) { cf = typedFactory; } else { cf = fallbackFactory (); } return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); } /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public override T Run (Func? errorHandler = null, IConsoleDriver? driver = null) { var top = new T (); Run (top, errorHandler); return top; } /// public override void Run (Toplevel view, Func? errorHandler = null) { Logging.Information ($"Run '{view}'"); ArgumentNullException.ThrowIfNull (view); if (!Application.Initialized) { throw new NotInitializedException (nameof (Run)); } if (Application.Driver == null) { // See Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); } Application.Top = view; RunState rs = Application.Begin (view); Application.Top.Running = true; // QUESTION: how to know when we are done? - ANSWER: Running == false while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running) { if (_coordinator is null) { throw new ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run"); } _coordinator.RunIteration (); } Logging.Information ($"Run - Calling End"); Application.End (rs); } /// public override void Shutdown () { _coordinator?.Stop (); base.Shutdown (); Application.Driver = null; } /// public override void RequestStop (Toplevel? top) { Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'"); top ??= Application.Top; if (top == null) { return; } var ev = new ToplevelClosingEventArgs (top); top.OnClosing (ev); if (ev.Cancel) { return; } // All RequestStop does is set the Running property to false - In the next iteration // this will be detected top.Running = false; } /// public override void Invoke (Action action) { // If we are already on the main UI thread if (Application.MainThreadId == Thread.CurrentThread.ManagedThreadId) { action (); return; } _timedEvents.Add (TimeSpan.Zero, () => { action (); return false; } ); } /// public override object AddTimeout (TimeSpan time, Func callback) { return _timedEvents.Add (time, callback); } /// public override bool RemoveTimeout (object token) { return _timedEvents.Remove (token); } /// public override void LayoutAndDraw (bool forceDraw) { // No more ad-hoc drawing, you must wait for iteration to do it Application.Top?.SetNeedsDraw(); Application.Top?.SetNeedsLayout (); } }