From 16bdc1ed408572ca024151a5f0ee44e3a4194e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Sat, 12 Aug 2023 09:09:17 +0200 Subject: [PATCH] Force rerender on visibility change --- src/ConsoleApp/FileTime.ConsoleUI.App/App.cs | 4 +- .../FileTime.ConsoleUI.App/Startup.cs | 14 -- src/ConsoleApp/FileTime.ConsoleUI/DI.cs | 2 + .../FileTime.ConsoleUI.csproj | 1 + src/FileTime.sln | 7 + .../TerminalUI.DependencyInjection.csproj | 17 +++ .../TerminalUiServiceCollectionExtensions.cs | 16 ++ src/Library/TerminalUI/ApplicationContext.cs | 21 ++- .../TerminalUI/Controls/ChildContainerView.cs | 15 +- src/Library/TerminalUI/Controls/Grid.cs | 53 ++++--- src/Library/TerminalUI/Controls/IView.cs | 1 + src/Library/TerminalUI/Controls/ListView.cs | 4 +- src/Library/TerminalUI/Controls/TextBox.cs | 2 +- src/Library/TerminalUI/Controls/View.cs | 34 +++-- src/Library/TerminalUI/EventLoop.cs | 70 +-------- src/Library/TerminalUI/IApplicationContext.cs | 10 +- src/Library/TerminalUI/IEventLoop.cs | 8 +- src/Library/TerminalUI/IRenderEngine.cs | 11 ++ src/Library/TerminalUI/RenderEngine.cs | 141 ++++++++++++++++++ .../Traits/IVisibilityChangeHandler.cs | 8 + 20 files changed, 292 insertions(+), 147 deletions(-) create mode 100644 src/Library/TerminalUI.DependencyInjection/TerminalUI.DependencyInjection.csproj create mode 100644 src/Library/TerminalUI.DependencyInjection/TerminalUiServiceCollectionExtensions.cs create mode 100644 src/Library/TerminalUI/IRenderEngine.cs create mode 100644 src/Library/TerminalUI/RenderEngine.cs create mode 100644 src/Library/TerminalUI/Traits/IVisibilityChangeHandler.cs diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index e8ebe26..86880c7 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -57,7 +57,7 @@ public class App : IApplication foreach (var rootView in _mainWindow.RootViews()) { - _applicationContext.EventLoop.AddViewToRender(rootView); + _applicationContext.RenderEngine.AddViewToPermanentRenderGroup(rootView); } _applicationContext.IsRunning = true; @@ -101,5 +101,5 @@ public class App : IApplication } } - private void Render() => _applicationContext.EventLoop.Run(); + private void Render() => _applicationContext.RenderEngine.Run(); } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs index 4b45525..27bf589 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -1,5 +1,4 @@ using FileTime.App.Core.Configuration; -using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.Configuration; @@ -10,9 +9,6 @@ using FileTime.Core.Interactions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using TerminalUI; -using TerminalUI.ConsoleDrivers; namespace FileTime.ConsoleUI.App; @@ -30,18 +26,8 @@ public static class Startup services.AddSingleton(); services.TryAddSingleton(new ApplicationConfiguration(true)); services.TryAddSingleton(); - services.TryAddSingleton(); services.Configure(configuration); - - services.TryAddSingleton(sp - => new ApplicationContext - { - ConsoleDriver = sp.GetRequiredService(), - LoggerFactory = sp.GetRequiredService(), - FocusManager = sp.GetRequiredService(), - } - ); return services; } diff --git a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs index d7d4273..93ff586 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs @@ -11,6 +11,7 @@ using FileTime.Tools.Compression; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Serilog; +using TerminalUI.DependencyInjection; namespace FileTime.ConsoleUI; @@ -23,6 +24,7 @@ public static class DI .RegisterDefaultServices(configuration: configuration) .AddConsoleServices(configuration) .AddConsoleViews() + .AddTerminalUi() .AddLocalProviderServices() .AddServerCoreServices() .AddFrequencyNavigation() diff --git a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj index 0fbc906..d5ace5f 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj @@ -15,6 +15,7 @@ + diff --git a/src/FileTime.sln b/src/FileTime.sln index a00444f..c13f61b 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Exte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralInputKey", "Library\GeneralInputKey\GeneralInputKey.csproj", "{91AE5B64-042B-4660-A8E8-D247E6E14A1E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.DependencyInjection", "Library\TerminalUI.DependencyInjection\TerminalUI.DependencyInjection.csproj", "{E72F6430-0E6E-4818-BD5F-114893ACB18E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -345,6 +347,10 @@ Global {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Release|Any CPU.Build.0 = Release|Any CPU + {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -404,6 +410,7 @@ Global {CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {E72F6430-0E6E-4818-BD5F-114893ACB18E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/TerminalUI.DependencyInjection/TerminalUI.DependencyInjection.csproj b/src/Library/TerminalUI.DependencyInjection/TerminalUI.DependencyInjection.csproj new file mode 100644 index 0000000..d046cd9 --- /dev/null +++ b/src/Library/TerminalUI.DependencyInjection/TerminalUI.DependencyInjection.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/src/Library/TerminalUI.DependencyInjection/TerminalUiServiceCollectionExtensions.cs b/src/Library/TerminalUI.DependencyInjection/TerminalUiServiceCollectionExtensions.cs new file mode 100644 index 0000000..a1a001d --- /dev/null +++ b/src/Library/TerminalUI.DependencyInjection/TerminalUiServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace TerminalUI.DependencyInjection; + +public static class TerminalUiServiceCollectionExtensions +{ + public static IServiceCollection AddTerminalUi(this IServiceCollection collection) + { + collection.TryAddSingleton(); + collection.TryAddSingleton(); + collection.TryAddSingleton(); + collection.TryAddSingleton(); + return collection; + } +} diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index 2f2592f..ffd98ae 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using TerminalUI.ConsoleDrivers; @@ -6,15 +7,23 @@ namespace TerminalUI; public class ApplicationContext : IApplicationContext { - public required IConsoleDriver ConsoleDriver { get; init; } - public required IFocusManager FocusManager { get; init; } - public ILoggerFactory? LoggerFactory { get; init; } - public IEventLoop EventLoop { get; init; } + private readonly Lazy _consoleDriver; + private readonly Lazy _focusManager; + private readonly Lazy _loggerFactory; + private readonly Lazy _renderEngine; + + public IConsoleDriver ConsoleDriver => _consoleDriver.Value; + public IFocusManager FocusManager => _focusManager.Value; + public ILoggerFactory? LoggerFactory => _loggerFactory.Value; + public IRenderEngine RenderEngine => _renderEngine.Value; public bool IsRunning { get; set; } public char EmptyCharacter { get; init; } = ' '; - public ApplicationContext() + public ApplicationContext(IServiceProvider serviceProvider) { - EventLoop = new EventLoop(this); + _consoleDriver = new Lazy(serviceProvider.GetRequiredService); + _focusManager = new Lazy(serviceProvider.GetRequiredService); + _loggerFactory = new Lazy(serviceProvider.GetService); + _renderEngine = new Lazy(serviceProvider.GetRequiredService); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ChildContainerView.cs b/src/Library/TerminalUI/Controls/ChildContainerView.cs index 381f3cc..c276f7c 100644 --- a/src/Library/TerminalUI/Controls/ChildContainerView.cs +++ b/src/Library/TerminalUI/Controls/ChildContainerView.cs @@ -7,7 +7,6 @@ namespace TerminalUI.Controls; public abstract class ChildContainerView : View, IChildContainer { private readonly ObservableCollection _children = new(); - private readonly Dictionary _visibilities = new(); public ReadOnlyObservableCollection Children { get; } public ChildInitializer ChildInitializer { get; } @@ -27,7 +26,7 @@ public abstract class ChildContainerView : View, IChildContainer } } - ApplicationContext?.EventLoop.RequestRerender(); + ApplicationContext?.RenderEngine.RequestRerender(this); } }; @@ -43,18 +42,6 @@ public abstract class ChildContainerView : View, IChildContainer }; } - protected void SaveVisibilities() - { - _visibilities.Clear(); - foreach (var child in _children) - { - _visibilities[child] = child.IsVisible; - } - } - - protected bool? GetLastVisibility(IView view) - => _visibilities.TryGetValue(view, out var visibility) ? visibility : null; - public override TChild AddChild(TChild child) { child = base.AddChild(child); diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index 6e94ed7..e103766 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -2,14 +2,17 @@ using Microsoft.Extensions.Logging; using TerminalUI.Extensions; using TerminalUI.Models; +using TerminalUI.Traits; using TerminalUI.ViewExtensions; namespace TerminalUI.Controls; -public class Grid : ChildContainerView +public class Grid : ChildContainerView, IVisibilityChangeHandler { private List _rowDefinitions = new() {RowDefinition.Star(1)}; private List _columnDefinitions = new() {ColumnDefinition.Star(1)}; + private List _forceRerenderChildren = new(); + private readonly object _forceRerenderChildrenLock = new(); private ILogger>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger>(); private delegate void WithSizes(RenderContext renderContext, ReadOnlySpan widths, ReadOnlySpan heights); @@ -150,6 +153,13 @@ public class Grid : ChildContainerView new Option(size, true), (context, columnWidths, rowHeights) => { + IReadOnlyList forceRerenderChildren; + lock (_forceRerenderChildrenLock) + { + forceRerenderChildren = _forceRerenderChildren; + _forceRerenderChildren.Clear(); + } + context = new RenderContext( context.ConsoleDriver, context.ForceRerender, @@ -169,7 +179,8 @@ public class Grid : ChildContainerView rowHeights, viewsByPosition, column, - row + row, + forceRerenderChildren ); } } @@ -177,29 +188,17 @@ public class Grid : ChildContainerView return true; }); - private void RenderViewsByPosition( - RenderContext context, + private void RenderViewsByPosition(RenderContext context, Position gridPosition, ReadOnlySpan columnWidths, ReadOnlySpan rowHeights, IReadOnlyDictionary<(int, int), List> viewsByPosition, int column, - int row) + int row, + IReadOnlyList forceRerenderChildren) { if (!viewsByPosition.TryGetValue((column, row), out var children)) return; - var anyChangedVisibility = false; - - foreach (var child in children) - { - var lastVisibility = GetLastVisibility(child); - if (lastVisibility is { } b && b != child.IsVisible) - { - anyChangedVisibility = true; - break; - } - } - var width = columnWidths[column]; var height = rowHeights[row]; var renderSize = new Size(width, height); @@ -212,7 +211,7 @@ public class Grid : ChildContainerView row ); - var needsRerender = anyChangedVisibility; + var needsRerender = children.Any(forceRerenderChildren.Contains); if (needsRerender) { context = new RenderContext( @@ -232,7 +231,7 @@ public class Grid : ChildContainerView { needsRerender = true; context = new RenderContext( - context.ConsoleDriver, + context.ConsoleDriver, true, context.Foreground, context.Background @@ -483,4 +482,20 @@ public class Grid : ChildContainerView ColumnDefinitions = columnDefinitions; } + + public void ChildVisibilityChanged(IView child) + { + var viewToForceRerender = child; + while (viewToForceRerender.VisualParent != null && viewToForceRerender.VisualParent != this) + { + viewToForceRerender = viewToForceRerender.VisualParent; + } + + if (viewToForceRerender.VisualParent != this) return; + + lock (_forceRerenderChildrenLock) + { + _forceRerenderChildren.Add(viewToForceRerender); + } + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index b703a51..dc877fb 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -27,6 +27,7 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection IApplicationContext? ApplicationContext { get; set; } List Extensions { get; } RenderMethod RenderMethod { get; set; } + IView? VisualParent { get; set; } event Action Disposed; Size GetRequestedSize(); diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index b07cd3b..afff38b 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -102,14 +102,14 @@ public partial class ListView : View if (_itemsSource is ObservableCollection observableDeclarative) { ((INotifyCollectionChanged) observableDeclarative).CollectionChanged += - (_, _) => ApplicationContext?.EventLoop.RequestRerender(); + (_, _) => ApplicationContext?.RenderEngine.RequestRerender(this); _getItems = () => observableDeclarative; } else if (_itemsSource is ReadOnlyObservableCollection readOnlyObservableDeclarative) { ((INotifyCollectionChanged) readOnlyObservableDeclarative).CollectionChanged += - (_, _) => ApplicationContext?.EventLoop.RequestRerender(); + (_, _) => ApplicationContext?.RenderEngine.RequestRerender(this); _getItems = () => readOnlyObservableDeclarative; } diff --git a/src/Library/TerminalUI/Controls/TextBox.cs b/src/Library/TerminalUI/Controls/TextBox.cs index 376d82f..79ee749 100644 --- a/src/Library/TerminalUI/Controls/TextBox.cs +++ b/src/Library/TerminalUI/Controls/TextBox.cs @@ -124,7 +124,7 @@ public partial class TextBox : View, IFocusable HandleKeyInputInternal(keyEventArgs); if (keyEventArgs.Handled) { - ApplicationContext?.EventLoop.RequestRerender(); + ApplicationContext?.RenderEngine.RequestRerender(this); } } diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 7966c0c..ea7b40e 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -30,7 +30,8 @@ public abstract partial class View : IView [Notify] private string? _name; [Notify] private IApplicationContext? _applicationContext; [Notify] private bool _attached; - + [Notify] private IView? _visualParent; + protected ObservableCollection VisualChildren { get; } = new(); public List Extensions { get; } = new(); @@ -90,7 +91,7 @@ public abstract partial class View : IView ) ) { - ApplicationContext?.EventLoop.RequestRerender(); + ApplicationContext?.RenderEngine.RequestRerender(this); } if (e.PropertyName == nameof(Attached)) @@ -100,13 +101,17 @@ public abstract partial class View : IView visualChild.Attached = Attached; } } - else if(e.PropertyName == nameof(ApplicationContext)) + else if (e.PropertyName == nameof(ApplicationContext)) { foreach (var visualChild in VisualChildren) { visualChild.ApplicationContext = ApplicationContext; } } + else if (e.PropertyName == nameof(IsVisible)) + { + ApplicationContext?.RenderEngine.VisibilityChanged(this); + } } protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size); @@ -221,7 +226,7 @@ public abstract partial class View : IView Size size) { var contentString = new string(content, size.Width); - + for (var i = 0; i < size.Height; i++) { var currentPosition = position with {Y = position.Y + i}; @@ -234,7 +239,7 @@ public abstract partial class View : IView protected void SetColorsForDriver(RenderContext renderContext) { var driver = renderContext.ConsoleDriver; - + var foreground = Foreground ?? renderContext.Foreground; var background = Background ?? renderContext.Background; if (foreground is not null) @@ -264,12 +269,8 @@ public abstract partial class View : IView public virtual TChild AddChild(TChild child) where TChild : IView { child.DataContext = DataContext; - CopyCommonPropertiesToNewChild(child); - VisualChildren.Add(child); - var mapper = new DataContextMapper(this, child, d => d); - AddDisposable(mapper); - child.AddDisposable(mapper); + SetupNewChild(child, mapper); return child; } @@ -278,20 +279,21 @@ public abstract partial class View : IView where TChild : IView { child.DataContext = dataContextMapper(DataContext); - CopyCommonPropertiesToNewChild(child); - VisualChildren.Add(child); - var mapper = new DataContextMapper(this, child, dataContextMapper); - AddDisposable(mapper); - child.AddDisposable(mapper); + SetupNewChild(child, mapper); return child; } - private void CopyCommonPropertiesToNewChild(IView child) + private void SetupNewChild(IView child, IDisposable dataContextmapper) { child.ApplicationContext = ApplicationContext; child.Attached = Attached; + child.VisualParent = this; + VisualChildren.Add(child); + + AddDisposable(dataContextmapper); + child.AddDisposable(dataContextmapper); } public virtual void RemoveChild(IView child) diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index 6f169c8..1f2d17c 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -1,86 +1,32 @@ -using TerminalUI.Controls; -using TerminalUI.Models; - -namespace TerminalUI; +namespace TerminalUI; public class EventLoop : IEventLoop { private readonly IApplicationContext _applicationContext; - private readonly object _lock = new(); - private readonly List _viewsToRender = new(); - private bool _rerenderRequested; - private bool _lastCursorVisible; + private readonly List _permanentQueue = new(); public EventLoop(IApplicationContext applicationContext) { _applicationContext = applicationContext; } + public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action); + public void Run() { _applicationContext.IsRunning = true; - _rerenderRequested = true; while (_applicationContext.IsRunning) { - Render(); + ProcessQueues(); Thread.Sleep(10); } } - public void RequestRerender() + private void ProcessQueues() { - lock (_lock) + foreach (var action in _permanentQueue) { - _rerenderRequested = true; - } - } - - public void Render() - { - List viewsToRender; - lock (_lock) - { - if (!_rerenderRequested) return; - _rerenderRequested = false; - viewsToRender = _viewsToRender.ToList(); - } - - var driver = _applicationContext.ConsoleDriver; - var size = driver.GetWindowSize(); - var renderContext = new RenderContext( - driver, - false, - null, - null - ); - foreach (var view in viewsToRender) - { - view.Attached = true; - view.GetRequestedSize(); - view.Render(renderContext, new Position(0, 0), size); - } - - if (_applicationContext.FocusManager.Focused is { } focused) - { - focused.SetCursorPosition(driver); - if (!_lastCursorVisible) - { - driver.SetCursorVisible(true); - _lastCursorVisible = true; - } - } - else if (_lastCursorVisible) - { - driver.SetCursorVisible(false); - _lastCursorVisible = false; - } - } - - public void AddViewToRender(IView view) - { - lock (_lock) - { - _viewsToRender.Add(view); + action(); } } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index d4f66e7..73c351b 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -6,10 +6,10 @@ namespace TerminalUI; public interface IApplicationContext { - IEventLoop EventLoop { get; init; } + IRenderEngine RenderEngine { get; } bool IsRunning { get; set; } - IConsoleDriver ConsoleDriver { get; init; } - ILoggerFactory? LoggerFactory { get; init; } - char EmptyCharacter { get; init; } - IFocusManager FocusManager { get; init; } + IConsoleDriver ConsoleDriver { get; } + ILoggerFactory? LoggerFactory { get; } + char EmptyCharacter { get; } + IFocusManager FocusManager { get; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IEventLoop.cs b/src/Library/TerminalUI/IEventLoop.cs index fc6dba5..be875e7 100644 --- a/src/Library/TerminalUI/IEventLoop.cs +++ b/src/Library/TerminalUI/IEventLoop.cs @@ -1,11 +1,7 @@ -using TerminalUI.Controls; - -namespace TerminalUI; +namespace TerminalUI; public interface IEventLoop { - void Render(); - void AddViewToRender(IView view); void Run(); - void RequestRerender(); + void AddToPermanentQueue(Action action); } \ No newline at end of file diff --git a/src/Library/TerminalUI/IRenderEngine.cs b/src/Library/TerminalUI/IRenderEngine.cs new file mode 100644 index 0000000..fa2f396 --- /dev/null +++ b/src/Library/TerminalUI/IRenderEngine.cs @@ -0,0 +1,11 @@ +using TerminalUI.Controls; + +namespace TerminalUI; + +public interface IRenderEngine +{ + void RequestRerender(IView view); + void VisibilityChanged(IView view); + void AddViewToPermanentRenderGroup(IView view); + void Run(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/RenderEngine.cs b/src/Library/TerminalUI/RenderEngine.cs new file mode 100644 index 0000000..4dd369b --- /dev/null +++ b/src/Library/TerminalUI/RenderEngine.cs @@ -0,0 +1,141 @@ +using TerminalUI.Controls; +using TerminalUI.Models; +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; + + public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) + { + _applicationContext = applicationContext; + _eventLoop = eventLoop; + + _eventLoop.AddToPermanentQueue(Render); + } + + public void RequestRerender(IView view) => RequestRerender(); + + public void VisibilityChanged(IView view) + { + 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); + } + else + { + visibilityChangeHandler.ChildVisibilityChanged(view); + } + } + + public void Run() => _eventLoop.Run(); + + public void RequestRerender() + { + lock (_lock) + { + _rerenderRequested = true; + } + } + + private void Render() + { + List permanentViewsToRender; + List forcedTemporaryViewsToRender; + lock (_lock) + { + if (!_rerenderRequested) return; + _rerenderRequested = false; + permanentViewsToRender = _permanentViewsToRender.ToList(); + forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList(); + } + + var driver = _applicationContext.ConsoleDriver; + var initialPosition = new Position(0, 0); + var size = driver.GetWindowSize(); + + RenderViews( + forcedTemporaryViewsToRender, + new RenderContext( + driver, + true, + null, + null + ), + initialPosition, + size); + + RenderViews( + permanentViewsToRender, + new RenderContext( + driver, + false, + null, + null + ), + initialPosition, + size); + + 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, RenderContext renderContext, Position position, Size size) + { + foreach (var view in views) + { + view.Attached = true; + view.GetRequestedSize(); + view.Render(renderContext, position, size); + } + } + + public void AddViewToPermanentRenderGroup(IView view) + { + lock (_lock) + { + _permanentViewsToRender.Add(view); + } + } + + public void AddViewToForcedTemporaryRenderGroup(IView view) + { + lock (_lock) + { + _forcedTemporaryViewsToRender.Add(view); + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Traits/IVisibilityChangeHandler.cs b/src/Library/TerminalUI/Traits/IVisibilityChangeHandler.cs new file mode 100644 index 0000000..3c56988 --- /dev/null +++ b/src/Library/TerminalUI/Traits/IVisibilityChangeHandler.cs @@ -0,0 +1,8 @@ +using TerminalUI.Controls; + +namespace TerminalUI.Traits; + +public interface IVisibilityChangeHandler +{ + void ChildVisibilityChanged(IView child); +} \ No newline at end of file