#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 IConsoleSizeMonitor? _consoleSizeMonitor;
///
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 IConsoleSizeMonitor ConsoleSizeMonitor
{
get => _consoleSizeMonitor ?? throw new NotInitializedException (nameof (ConsoleSizeMonitor));
private set => _consoleSizeMonitor = 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 ());
ConsoleSizeMonitor = componentFactory.CreateConsoleSizeMonitor (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.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (Application.Mouse.MouseGrabView));
bool sizeChanged = ConsoleSizeMonitor.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
}
}