#nullable enable using Terminal.Gui.Drivers; using System.Collections.Concurrent; using System.Diagnostics; 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 { private ITimedEvents? _timedEvents; private ConcurrentQueue? _inputBuffer; private IInputProcessor? _inputProcessor; private IConsoleOutput? _out; private AnsiRequestScheduler? _ansiRequestScheduler; private IWindowSizeMonitor? _windowSizeMonitor; /// 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 /// /// public ConcurrentQueue InputBuffer { get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer)); private set => _inputBuffer = value; } /// public IInputProcessor InputProcessor { get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor)); private set => _inputProcessor = value; } /// public IOutputBuffer OutputBuffer { get; } = new OutputBuffer (); /// public IConsoleOutput Out { get => _out ?? throw new NotInitializedException (nameof (Out)); private set => _out = value; } /// public AnsiRequestScheduler AnsiRequestScheduler { get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler)); private set => _ansiRequestScheduler = value; } /// public IWindowSizeMonitor WindowSizeMonitor { get => _windowSizeMonitor ?? throw new NotInitializedException (nameof (WindowSizeMonitor)); private set => _windowSizeMonitor = value; } /// /// Handles raising events and setting required draw status etc when changes /// public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); /// /// Determines how to get the current system type, adjust /// in unit tests to simulate specific timings. /// public Func Now { get; set; } = () => DateTime.Now; /// /// Initializes the class with the provided subcomponents /// /// /// /// /// /// public void Initialize ( ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput, IComponentFactory componentFactory ) { InputBuffer = inputBuffer; Out = consoleOutput; InputProcessor = inputProcessor; TimedEvents = timedEvents; AnsiRequestScheduler = new (InputProcessor.GetParser ()); WindowSizeMonitor = componentFactory.CreateWindowSizeMonitor (Out, OutputBuffer); } /// public void Iteration () { Application.RaiseIteration (); DateTime dt = Now (); int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond); IterationImpl (); TimeSpan took = Now () - dt; TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took; Logging.TotalIterationMetric.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { Task.Delay (sleepFor).Wait (); } } internal void IterationImpl () { InputProcessor.ProcessQueue (); ToplevelTransitionManager.RaiseReadyEventIfNeeded (); ToplevelTransitionManager.HandleTopMaybeChanging (); if (Application.Top != null) { bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) || AnySubViewsNeedDrawn (Application.Top) || (Application.MouseGrabHandler.MouseGrabView != null && AnySubViewsNeedDrawn (Application.MouseGrabHandler.MouseGrabView)); bool sizeChanged = WindowSizeMonitor.Poll (); if (needsDrawOrLayout || sizeChanged) { Logging.Redraws.Add (1); Application.LayoutAndDraw (true); Out.Write (OutputBuffer); Out.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); Out.SetCursorPosition (to.Value.X, to.Value.Y); Out.SetCursorVisibility (mostFocused.CursorVisibility); } else { Out.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 } }