mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
223 lines
6.8 KiB
C#
223 lines
6.8 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
|
|
namespace Terminal.Gui.App;
|
|
|
|
/// <summary>
|
|
/// The main application loop that runs Terminal.Gui's UI rendering and event processing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This class coordinates the Terminal.Gui application lifecycle by:
|
|
/// <list type="bullet">
|
|
/// <item>Processing buffered input events and translating them to UI events</item>
|
|
/// <item>Executing user timeout callbacks at scheduled intervals</item>
|
|
/// <item>Detecting which views need redrawing or layout updates</item>
|
|
/// <item>Rendering UI changes to the console output buffer</item>
|
|
/// <item>Managing cursor position and visibility</item>
|
|
/// <item>Throttling iterations to respect <see cref="Application.MaximumIterationsPerSecond"/></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
/// <typeparam name="TInputRecord">Type of raw input events, e.g. <see cref="ConsoleKeyInfo"/> for .NET driver</typeparam>
|
|
public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputRecord> where TInputRecord : struct
|
|
{
|
|
private ITimedEvents? _timedEvents;
|
|
private ConcurrentQueue<TInputRecord>? _inputQueue;
|
|
private IInputProcessor? _inputProcessor;
|
|
private IOutput? _output;
|
|
private AnsiRequestScheduler? _ansiRequestScheduler;
|
|
private ISizeMonitor? _sizeMonitor;
|
|
|
|
/// <inheritdoc/>
|
|
public IApplication? App { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public ITimedEvents TimedEvents
|
|
{
|
|
get => _timedEvents ?? throw new NotInitializedException (nameof (TimedEvents));
|
|
private set => _timedEvents = value;
|
|
}
|
|
|
|
// TODO: follow above pattern for others too
|
|
|
|
/// <summary>
|
|
/// The input events thread-safe collection. This is populated on separate
|
|
/// thread by a <see cref="IInput{T}"/>. Is drained as part of each
|
|
/// <see cref="Iteration"/> on the main loop thread.
|
|
/// </summary>
|
|
public ConcurrentQueue<TInputRecord> InputQueue
|
|
{
|
|
get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue));
|
|
private set => _inputQueue = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IInputProcessor InputProcessor
|
|
{
|
|
get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor));
|
|
private set => _inputProcessor = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl ();
|
|
|
|
/// <inheritdoc/>
|
|
public IOutput Output
|
|
{
|
|
get => _output ?? throw new NotInitializedException (nameof (Output));
|
|
private set => _output = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public AnsiRequestScheduler AnsiRequestScheduler
|
|
{
|
|
get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler));
|
|
private set => _ansiRequestScheduler = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ISizeMonitor SizeMonitor
|
|
{
|
|
get => _sizeMonitor ?? throw new NotInitializedException (nameof (SizeMonitor));
|
|
private set => _sizeMonitor = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the class with the provided subcomponents
|
|
/// </summary>
|
|
/// <param name="timedEvents"></param>
|
|
/// <param name="inputBuffer"></param>
|
|
/// <param name="inputProcessor"></param>
|
|
/// <param name="consoleOutput"></param>
|
|
/// <param name="componentFactory"></param>
|
|
/// <param name="app"></param>
|
|
public void Initialize (
|
|
ITimedEvents timedEvents,
|
|
ConcurrentQueue<TInputRecord> inputBuffer,
|
|
IInputProcessor inputProcessor,
|
|
IOutput consoleOutput,
|
|
IComponentFactory<TInputRecord> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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));
|
|
|
|
SizeMonitor.Poll ();
|
|
|
|
if (needsDrawOrLayout)
|
|
{
|
|
Logging.Redraws.Add (1);
|
|
|
|
App?.LayoutAndDraw (forceRedraw: false);
|
|
|
|
Output.Write (OutputBuffer);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose ()
|
|
{
|
|
// TODO release managed resources here
|
|
}
|
|
}
|