using TerminalUI.Controls; using TerminalUI.Models; using TerminalUI.Styling; using TerminalUI.TextFormat; using TerminalUI.Traits; namespace TerminalUI; public class RenderEngine : IRenderEngine { private readonly IApplicationContext _applicationContext; private readonly IEventLoop _eventLoop; private readonly object _lock = new(); private readonly List _permanentViewsToRender = new(); private readonly List _forcedTemporaryViewsToRender = new(); private bool _rerenderRequested = true; private bool _lastCursorVisible; private bool _forceRerenderAll; private bool[,]? _updatedCells; private bool[,]? _filledCells; private bool[,]? _lastFilledCells; private DateTime _renderRequestDetected; private ITheme? _lastTheme; public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) { _applicationContext = applicationContext; _eventLoop = eventLoop; _eventLoop.AddToPermanentQueue(Render); _eventLoop.AddInitializer(() => { _applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId; _applicationContext.ConsoleDriver.EnterRestrictedMode(); }); } public void RequestRerender(IView view) => RequestRerender(); public void VisibilityChanged(IView view, bool newVisibility) { if (!newVisibility) { _forceRerenderAll = true; return; } IVisibilityChangeHandler? visibilityChangeHandler = null; var parent = view.VisualParent; while (parent?.VisualParent != null) { if (parent is IVisibilityChangeHandler v) { visibilityChangeHandler = v; break; } parent = parent.VisualParent; } if (visibilityChangeHandler is null) { AddViewToForcedTemporaryRenderGroup(parent ?? view); return; } visibilityChangeHandler.ChildVisibilityChanged(view); } public void Run() => _eventLoop.Run(); public void RequestRerender() { lock (_lock) { _rerenderRequested = true; } } private void Render() { List permanentViewsToRender; List forcedTemporaryViewsToRender; bool forceRerenderAll; lock (_lock) { if (!_rerenderRequested) return; _rerenderRequested = false; permanentViewsToRender = _permanentViewsToRender.ToList(); forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList(); _forcedTemporaryViewsToRender.Clear(); forceRerenderAll = _forceRerenderAll; _forceRerenderAll = false; } if (_applicationContext.Theme != _lastTheme) { _lastTheme = _applicationContext.Theme; forceRerenderAll = true; } var driver = _applicationContext.ConsoleDriver; var initialPosition = new Position(0, 0); var size = driver.GetWindowSize(); //TODO: this could be stack allocated when sizes are small if (_updatedCells is null || _updatedCells.GetLength(0) != size.Width || _updatedCells.GetLength(1) != size.Height) { _updatedCells = new bool[size.Width, size.Height]; } else { ClearArray2D(_updatedCells); } if (!forceRerenderAll) { RenderViews( forcedTemporaryViewsToRender, new RenderContext( driver, true, null, null, new RenderStatistics(), new TextFormatContext(driver.SupportsAnsiEscapeSequence), _updatedCells ), initialPosition, size); } RenderViews( permanentViewsToRender, new RenderContext( driver, forceRerenderAll, null, null, new RenderStatistics(), new TextFormatContext(driver.SupportsAnsiEscapeSequence), _updatedCells ), initialPosition, size); if (_lastFilledCells is not null && _lastFilledCells.GetLength(0) == size.Width && _lastFilledCells.GetLength(1) == size.Height) { Array2DHelper.CombineArray2Ds( _updatedCells, _lastFilledCells, new Position(0, 0), _updatedCells, (a, b) => (a ?? false) || (b ?? false) ); } if (_filledCells is null || _filledCells.GetLength(0) != size.Width || _filledCells.GetLength(1) != size.Height) { _filledCells = new bool[size.Width, size.Height]; } else { ClearArray2D(_filledCells); } driver.ResetStyle(); Array2DHelper.RenderEmpty( driver, _updatedCells, _filledCells, _applicationContext.EmptyCharacter, initialPosition, size); (_lastFilledCells, _filledCells) = (_filledCells, _lastFilledCells); if (_applicationContext.FocusManager.Focused is { } focused) { focused.SetCursorPosition(driver); if (!_lastCursorVisible) { driver.SetCursorVisible(true); _lastCursorVisible = true; } } else if (_lastCursorVisible) { driver.SetCursorVisible(false); _lastCursorVisible = false; } } private void RenderViews(List views, in RenderContext renderContext, Position position, Size size) { foreach (var view in views) { view.Attached = true; view.GetRequestedSize(); view.Render(renderContext, position, size); } } private void ClearArray2D(T[,] array, T defaultValue = default!) { var maxX = array.GetLength(0); var maxY = array.GetLength(1); for (var x = 0; x < maxX; x++) { for (var y = 0; y < maxY; y++) { array[x, y] = defaultValue; } } } public void AddViewToPermanentRenderGroup(IView view) { lock (_lock) { _permanentViewsToRender.Add(view); } } public void AddViewToForcedTemporaryRenderGroup(IView view) { lock (_lock) { _forcedTemporaryViewsToRender.Add(view); } } }