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 where TInputRecord : struct
{
private ITimedEvents? _timedEvents;
private ConcurrentQueue? _inputQueue;
private IInputProcessor? _inputProcessor;
private IOutput? _output;
private AnsiRequestScheduler? _ansiRequestScheduler;
private ISizeMonitor? _sizeMonitor;
///
public IApplication? App { get; private set; }
///
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;
}
///
/// Initializes the class with the provided subcomponents
///
///
///
///
///
///
///
public void Initialize (
ITimedEvents timedEvents,
ConcurrentQueue inputBuffer,
IInputProcessor inputProcessor,
IOutput consoleOutput,
IComponentFactory componentFactory,
IApplication? app
)
{
App = app;
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 ()
{
App?.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 ();
if (App?.TopRunnableView != null)
{
bool needsDrawOrLayout = AnySubViewsNeedDrawn (App?.Popover?.GetActivePopover () as View)
|| AnySubViewsNeedDrawn (App?.TopRunnableView)
|| (App?.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (App?.Mouse.MouseGrabView));
bool sizeChanged = SizeMonitor.Poll ();
if (needsDrawOrLayout || sizeChanged)
{
Logging.Redraws.Add (1);
App?.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 = App?.TopRunnableView!.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
}
}