#nullable enable using System; using System.Collections.Concurrent; using System.Diagnostics; using Terminal.Gui.Drivers; namespace Terminal.Gui.App; /// /// The main application loop that runs Terminal.Gui's UI rendering and event processing. /// /// /// This class coordinates the Terminal.Gui application lifecycle by: /// /// Processing buffered input events and translating them to UI events /// Executing user timeout callbacks at scheduled intervals /// Detecting which views need redrawing or layout updates /// Rendering UI changes to the console output buffer /// Managing cursor position and visibility /// Throttling iterations to respect /// /// /// Type of raw input events, e.g. for .NET driver public class ApplicationMainLoop : IApplicationMainLoop where TInputRecord : struct { private ITimedEvents? _timedEvents; private ConcurrentQueue? _inputQueue; private IInputProcessor? _inputProcessor; private IOutput? _output; private AnsiRequestScheduler? _ansiRequestScheduler; private ISizeMonitor? _sizeMonitor; /// public ITimedEvents TimedEvents { get => _timedEvents ?? throw new NotInitializedException (nameof (TimedEvents)); private set => _timedEvents = value; } // TODO: follow above pattern for others too /// /// The input events thread-safe collection. This is populated on separate /// thread by a . Is drained as part of each /// on the main loop thread. /// public ConcurrentQueue InputQueue { get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue)); private set => _inputQueue = value; } /// public IInputProcessor InputProcessor { get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor)); private set => _inputProcessor = value; } /// public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl (); /// public IOutput Output { get => _output ?? throw new NotInitializedException (nameof (Output)); private set => _output = value; } /// public AnsiRequestScheduler AnsiRequestScheduler { get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler)); private set => _ansiRequestScheduler = value; } /// public ISizeMonitor SizeMonitor { get => _sizeMonitor ?? throw new NotInitializedException (nameof (SizeMonitor)); private set => _sizeMonitor = value; } /// /// Handles raising events and setting required draw status etc when changes /// public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); /// /// Initializes the class with the provided subcomponents /// /// /// /// /// /// public void Initialize ( ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IOutput consoleOutput, IComponentFactory componentFactory ) { InputQueue = inputBuffer; Output = consoleOutput; InputProcessor = inputProcessor; TimedEvents = timedEvents; AnsiRequestScheduler = new (InputProcessor.GetParser ()); OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height); SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer); } /// public void Iteration () { Application.RaiseIteration (); DateTime dt = DateTime.Now; int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond); IterationImpl (); TimeSpan took = DateTime.Now - dt; TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took; Logging.TotalIterationMetric.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { Task.Delay (sleepFor).Wait (); } } internal void IterationImpl () { // Pull any input events from the input queue and process them InputProcessor.ProcessQueue (); ToplevelTransitionManager.RaiseReadyEventIfNeeded (); ToplevelTransitionManager.HandleTopMaybeChanging (); if (Application.Top != null) { bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) || AnySubViewsNeedDrawn (Application.Top) || (Application.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (Application.Mouse.MouseGrabView)); bool sizeChanged = SizeMonitor.Poll (); if (needsDrawOrLayout || sizeChanged) { Logging.Redraws.Add (1); Application.LayoutAndDraw (true); Output.Write (OutputBuffer); Output.SetCursorVisibility (CursorVisibility.Default); } SetCursor (); } var swCallbacks = Stopwatch.StartNew (); TimedEvents.RunTimers (); Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } private void SetCursor () { View? mostFocused = Application.Top!.MostFocused; if (mostFocused == null) { return; } Point? to = mostFocused.PositionCursor (); if (to.HasValue) { // Translate to screen coordinates to = mostFocused.ViewportToScreen (to.Value); Output.SetCursorPosition (to.Value.X, to.Value.Y); Output.SetCursorVisibility (mostFocused.CursorVisibility); } else { Output.SetCursorVisibility (CursorVisibility.Invisible); } } private bool AnySubViewsNeedDrawn (View? v) { if (v is null) { return false; } if (v.NeedsDraw || v.NeedsLayout) { // Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); return true; } foreach (View subview in v.SubViews) { if (AnySubViewsNeedDrawn (subview)) { return true; } } return false; } /// public void Dispose () { // TODO release managed resources here } }