From 52536b569d1c8b66e70975493891a2f6dd42a24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 8 Aug 2023 12:49:15 +0200 Subject: [PATCH] EventLoop V1 --- src/ConsoleApp/FileTime.ConsoleUI.App/App.cs | 9 +- .../FileTime.ConsoleUI.App/MainWindow.cs | 25 +++- .../FileTime.ConsoleUI.App/Startup.cs | 2 + .../FileTime.Core.Services.csproj | 1 + src/Core/FileTime.Core.Services/Tab.cs | 2 +- src/FileTime.sln | 7 + src/Library/TerminalUI/ApplicationContext.cs | 12 ++ src/Library/TerminalUI/Binding.cs | 97 ++++++++++--- .../TerminalUI/Controls/ContentView.cs | 15 ++ src/Library/TerminalUI/Controls/IView.cs | 20 ++- src/Library/TerminalUI/Controls/ListView.cs | 44 ++++-- .../TerminalUI/Controls/ListViewItem.cs | 20 ++- src/Library/TerminalUI/Controls/TextBlock.cs | 23 ++++ src/Library/TerminalUI/Controls/View.cs | 84 ++++++++--- src/Library/TerminalUI/EventLoop.cs | 130 ++++++++++++++++++ src/Library/TerminalUI/Extensions/Binding.cs | 22 +-- src/Library/TerminalUI/IApplicationContext.cs | 7 + src/Library/TerminalUI/IEventLoop.cs | 10 ++ src/Library/TerminalUI/TerminalUI.csproj | 7 + .../TerminalUI/Traits/IContentRenderer.cs | 9 ++ .../Traits/IDisposableCollection.cs | 1 + 21 files changed, 479 insertions(+), 68 deletions(-) create mode 100644 src/Library/TerminalUI/ApplicationContext.cs create mode 100644 src/Library/TerminalUI/Controls/ContentView.cs create mode 100644 src/Library/TerminalUI/Controls/TextBlock.cs create mode 100644 src/Library/TerminalUI/EventLoop.cs create mode 100644 src/Library/TerminalUI/IApplicationContext.cs create mode 100644 src/Library/TerminalUI/IEventLoop.cs create mode 100644 src/Library/TerminalUI/Traits/IContentRenderer.cs diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index e338921..a7b1d8a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -1,5 +1,6 @@ using FileTime.App.Core.Services; using FileTime.ConsoleUI.App.KeyInputHandling; +using TerminalUI; namespace FileTime.ConsoleUI.App; @@ -9,6 +10,7 @@ public class App : IApplication private readonly IConsoleAppState _consoleAppState; //private readonly IAppKeyService _appKeyService; private readonly MainWindow _mainWindow; + private readonly IApplicationContext _applicationContext; private readonly IKeyInputHandlerService _keyInputHandlerService; public App( @@ -16,20 +18,23 @@ public class App : IApplication IKeyInputHandlerService keyInputHandlerService, IConsoleAppState consoleAppState, //IAppKeyService appKeyService, - MainWindow mainWindow) + MainWindow mainWindow, + IApplicationContext applicationContext) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; _consoleAppState = consoleAppState; //_appKeyService = appKeyService; _mainWindow = mainWindow; + _applicationContext = applicationContext; } public void Run() { - Console.WriteLine("Loading..."); Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait(); _mainWindow.Initialize(); + + _applicationContext.EventLoop.Run(); } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs index 302f38b..d822397 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,7 +1,6 @@ -using System.Collections.ObjectModel; +using System.Linq.Expressions; using DeclarativeProperty; using FileTime.App.Core.ViewModels; -using FileTime.ConsoleUI.App.Extensions; using TerminalUI; using TerminalUI.Controls; using TerminalUI.Extensions; @@ -11,23 +10,39 @@ namespace FileTime.ConsoleUI.App; public class MainWindow { private readonly IConsoleAppState _consoleAppState; + private readonly IApplicationContext _applicationContext; private const int ParentColumnWidth = 20; - public MainWindow(IConsoleAppState consoleAppState) + public MainWindow(IConsoleAppState consoleAppState, IApplicationContext applicationContext) { _consoleAppState = consoleAppState; + _applicationContext = applicationContext; } public void Initialize() { - ListView selectedItemsView = new(); + ListView selectedItemsView = new() + { + ApplicationContext = _applicationContext + }; selectedItemsView.DataContext = _consoleAppState; + selectedItemsView.ItemTemplate = item => + { + var textBlock = item.CreateChild>(); + textBlock.Bind( + textBlock, + dc => dc == null ? string.Empty : dc.DisplayNameText, + tb => tb.Text + ); + + return textBlock; + }; selectedItemsView.Bind( selectedItemsView, appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), v => v.ItemsSource); - selectedItemsView.Render(); + selectedItemsView.RequestRerender(); } } \ 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 69fa664..4e1c353 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -5,6 +5,7 @@ using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using TerminalUI; namespace FileTime.ConsoleUI.App; @@ -20,6 +21,7 @@ public static class Startup services.TryAddSingleton(); services.AddSingleton(); + services.TryAddSingleton(); return services; } } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj index 5b9f1b7..35164c0 100644 --- a/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj +++ b/src/Core/FileTime.Core.Services/FileTime.Core.Services.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index c316830..ea44171 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -1,8 +1,8 @@ using System.Collections.ObjectModel; +using CircularBuffer; using DeclarativeProperty; using DynamicData; using FileTime.App.Core.Services; -using FileTime.Core.Collections; using FileTime.Core.Helper; using FileTime.Core.Models; using FileTime.Core.Timeline; diff --git a/src/FileTime.sln b/src/FileTime.sln index fff18c0..561d26e 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.App.Abst EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\CircularBuffer\CircularBuffer.csproj", "{AF4FE804-12D9-46E2-A584-BFF6D4509766}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -321,6 +323,10 @@ Global {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Release|Any CPU.Build.0 = Release|Any CPU + {AF4FE804-12D9-46E2-A584-BFF6D4509766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF4FE804-12D9-46E2-A584-BFF6D4509766}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF4FE804-12D9-46E2-A584-BFF6D4509766}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -376,6 +382,7 @@ Global {826AFD32-E36B-48BA-BC1E-1476B393CF24} = {A5291117-3001-498B-AC8B-E14F71F72570} {81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs new file mode 100644 index 0000000..7bdb0ba --- /dev/null +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -0,0 +1,12 @@ +namespace TerminalUI; + +public class ApplicationContext : IApplicationContext +{ + public IEventLoop EventLoop { get; init; } + public bool IsRunning { get; set; } + + public ApplicationContext() + { + EventLoop = new EventLoop(this); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index b50bd5f..38b2a73 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Linq.Expressions; using System.Reflection; using TerminalUI.Controls; using TerminalUI.Traits; @@ -8,41 +9,103 @@ namespace TerminalUI; public class Binding : IDisposable { private readonly Func _dataContextMapper; - private IView _view; + private IView _dataSourceView; private object? _propertySource; private PropertyInfo _targetProperty; + private readonly List _rerenderProperties; + private readonly IDisposableCollection? _propertySourceDisposableCollection; + private INotifyPropertyChanged? _dataSourceLastDataContext; public Binding( - IView view, - Func dataContextMapper, - object? propertySource, - PropertyInfo targetProperty + IView dataSourceView, + Expression> dataContextExpression, + object? propertySource, + PropertyInfo targetProperty, + IEnumerable? rerenderProperties = null ) { - _view = view; - _dataContextMapper = dataContextMapper; + ArgumentNullException.ThrowIfNull(dataSourceView); + ArgumentNullException.ThrowIfNull(dataContextExpression); + ArgumentNullException.ThrowIfNull(targetProperty); + _dataSourceView = dataSourceView; + _dataContextMapper = dataContextExpression.Compile(); _propertySource = propertySource; _targetProperty = targetProperty; - view.PropertyChanged += View_PropertyChanged; - _targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext)); - - if(propertySource is IDisposableCollection disposableCollection) - disposableCollection.AddDisposable(this); - - view.AddDisposable(this); + _rerenderProperties = rerenderProperties?.ToList() ?? new List(); + + FindReactiveProperties(dataContextExpression); + + dataSourceView.PropertyChanged += View_PropertyChanged; + var initialValue = _dataContextMapper(_dataSourceView.DataContext); + _targetProperty.SetValue(_propertySource, initialValue); + + if (propertySource is IDisposableCollection propertySourceDisposableCollection) + { + propertySourceDisposableCollection.AddDisposable(this); + _propertySourceDisposableCollection = propertySourceDisposableCollection; + } + + if (_dataSourceView.DataContext is INotifyPropertyChanged dataSourcePropertyChanged) + { + _dataSourceLastDataContext = dataSourcePropertyChanged; + dataSourcePropertyChanged.PropertyChanged += DataContext_PropertyChanged; + } + + dataSourceView.AddDisposable(this); + } + + private void FindReactiveProperties(Expression expression) + { + if (expression is LambdaExpression lambdaExpression) + { + FindReactiveProperties(lambdaExpression.Body); + } + else if (expression is ConditionalExpression conditionalExpression) + { + FindReactiveProperties(conditionalExpression.IfFalse); + FindReactiveProperties(conditionalExpression.IfTrue); + } + else if (expression is MemberExpression {Member: PropertyInfo dataContextPropertyInfo}) + { + _rerenderProperties.Add(dataContextPropertyInfo.Name); + } + //TODO: Handle other expression types } private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IView.DataContext)) return; - _targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext)); + if (_dataSourceLastDataContext is not null) + { + _dataSourceLastDataContext.PropertyChanged -= DataContext_PropertyChanged; + } + + if (_dataSourceView.DataContext is INotifyPropertyChanged dataSourcePropertyChanged) + { + _dataSourceLastDataContext = dataSourcePropertyChanged; + dataSourcePropertyChanged.PropertyChanged += DataContext_PropertyChanged; + } + + UpdateTargetProperty(); } + private void DataContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == null + || !_rerenderProperties.Contains(e.PropertyName)) return; + UpdateTargetProperty(); + } + + private void UpdateTargetProperty() + => _targetProperty.SetValue(_propertySource, _dataContextMapper(_dataSourceView.DataContext)); + public void Dispose() { - _view.PropertyChanged -= View_PropertyChanged; - _view = null!; + _propertySourceDisposableCollection?.RemoveDisposable(this); + _dataSourceView.RemoveDisposable(this); + _dataSourceView.PropertyChanged -= View_PropertyChanged; + _dataSourceView = null!; _propertySource = null!; _targetProperty = null!; } diff --git a/src/Library/TerminalUI/Controls/ContentView.cs b/src/Library/TerminalUI/Controls/ContentView.cs new file mode 100644 index 0000000..0a2ba0b --- /dev/null +++ b/src/Library/TerminalUI/Controls/ContentView.cs @@ -0,0 +1,15 @@ +using TerminalUI.Traits; + +namespace TerminalUI.Controls; + +public abstract class ContentView: View, IContentRenderer +{ + protected ContentView() + { + ContentRendererMethod = DefaultContentRender; + } + public IView? Content { get; set; } + public Action ContentRendererMethod { get; set; } + + private void DefaultContentRender() => Content?.Render(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index 67e9a6c..beb36e1 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -3,10 +3,26 @@ using TerminalUI.Traits; namespace TerminalUI.Controls; -public interface IView : INotifyPropertyChanged, IDisposableCollection +public interface IView : INotifyPropertyChanged, IDisposableCollection { - T? DataContext { get; set; } + object? DataContext { get; set; } + Action RenderMethod { get; set; } + IApplicationContext ApplicationContext { get; init;} + event Action Disposed; + event Action RenderRequested; void Render(); + void RequestRerender(); +} + +public interface IView : IView +{ + new T? DataContext { get; set; } + + object? IView.DataContext + { + get => DataContext; + set => DataContext = (T?) value; + } TChild CreateChild() where TChild : IView, new(); diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index 71b5d28..4b4cbc5 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -52,12 +52,31 @@ public class ListView : View } } - public override void Render() + public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; + + protected override void DefaultRenderer() { - if (_getItems is null) return; + var listViewItems = InstantiateItemViews(); + foreach (var item in listViewItems) + { + item.Render(); + } + } + + private Span> InstantiateItemViews() + { + if (_getItems is null) + { + if (_listViewItemLength != 0) + { + return InstantiateEmptyItemViews(); + } + + return _listViewItems; + } var items = _getItems().ToList(); - Span> listViewItems = null; + Span> listViewItems; if (_listViewItems is null || _listViewItems.Length != items.Count) { @@ -65,7 +84,10 @@ public class ListView : View for (var i = 0; i < items.Count; i++) { var dataContext = items[i]; - newListViewItems[i] = CreateChild, TItem>(_ => dataContext); + var child = CreateChild, TItem>(_ => dataContext); + child.Content = ItemTemplate(child); + ItemTemplate(child); + newListViewItems[i] = child; } _listViewItems = newListViewItems; @@ -77,9 +99,15 @@ public class ListView : View listViewItems = _listViewItems[.._listViewItemLength]; } - foreach (var item in listViewItems) - { - item.Render(); - } + return listViewItems; } + + private Span> InstantiateEmptyItemViews() + { + _listViewItems = ListViewItemPool.Rent(0); + _listViewItemLength = 0; + return _listViewItems; + } + + private static IView? DefaultItemTemplate(ListViewItem listViewItem) => null; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs index 307a2a7..55c4092 100644 --- a/src/Library/TerminalUI/Controls/ListViewItem.cs +++ b/src/Library/TerminalUI/Controls/ListViewItem.cs @@ -1,9 +1,21 @@ -namespace TerminalUI.Controls; +using TerminalUI.Traits; -public class ListViewItem : View +namespace TerminalUI.Controls; + +public class ListViewItem : ContentView { - public override void Render() + protected override void DefaultRenderer() { - Console.WriteLine(DataContext?.ToString()); + if (ContentRendererMethod is null) + { + throw new NullReferenceException( + nameof(ContentRendererMethod) + + " is null, cannot render content of " + + Content?.GetType().Name + + " with DataContext of " + + DataContext?.GetType().Name); + } + + ContentRendererMethod(); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs new file mode 100644 index 0000000..4df745c --- /dev/null +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -0,0 +1,23 @@ +using PropertyChanged.SourceGenerator; +using TerminalUI.Extensions; + +namespace TerminalUI.Controls; + +public partial class TextBlock : View +{ + [Notify] private string? _text = string.Empty; + + public TextBlock() + { + this.Bind( + this, + dc => dc == null ? string.Empty : dc.ToString(), + tb => tb.Text + ); + + RerenderProperties.Add(nameof(Text)); + } + + protected override void DefaultRenderer() + => Console.Write(Text); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index ab93bb7..d8397f8 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -1,20 +1,63 @@ -using System.Collections.Concurrent; +using System.Buffers; using System.ComponentModel; using System.Runtime.CompilerServices; +using PropertyChanged.SourceGenerator; namespace TerminalUI.Controls; -public abstract class View : IView +public abstract partial class View : IView { - private readonly ConcurrentBag _disposables = new(); - public T? DataContext { get; set; } - public abstract void Render(); + private readonly List _disposables = new(); + [Notify] private T? _dataContext; + public Action RenderMethod { get; set; } + public IApplicationContext ApplicationContext { get; init; } + public event Action? Disposed; + public event Action? RenderRequested; + protected List RerenderProperties { get; } = new(); + + protected View() + { + RenderMethod = DefaultRenderer; + ((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged; + } + + private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not null + && (e.PropertyName == nameof(IView.DataContext) + || RerenderProperties.Contains(e.PropertyName) + ) + ) + { + RenderRequested?.Invoke(this); + } + } + + protected abstract void DefaultRenderer(); + + public void Render() + { + if (RenderMethod is null) + { + throw new NullReferenceException( + nameof(RenderMethod) + + " is null, cannot render content of " + + GetType().Name + + " with DataContext of " + + DataContext?.GetType().Name); + } + + RenderMethod(); + } + + public void RequestRerender() => RenderRequested?.Invoke(this); public TChild CreateChild() where TChild : IView, new() { var child = new TChild { - DataContext = DataContext + DataContext = DataContext, + ApplicationContext = ApplicationContext }; var mapper = new DataContextMapper(this, d => child.DataContext = d); AddDisposable(mapper); @@ -23,12 +66,13 @@ public abstract class View : IView return child; } - public TChild CreateChild(Func dataContextMapper) + public TChild CreateChild(Func dataContextMapper) where TChild : IView, new() { var child = new TChild { - DataContext = dataContextMapper(DataContext) + DataContext = dataContextMapper(DataContext), + ApplicationContext = ApplicationContext }; var mapper = new DataContextMapper(this, d => child.DataContext = dataContextMapper(d)); AddDisposable(mapper); @@ -38,36 +82,36 @@ public abstract class View : IView } public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable); + public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable); public event PropertyChangedEventHandler? PropertyChanged; - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - protected bool SetField(ref TProp field, TProp value, [CallerMemberName] string? propertyName = null) - { - if (EqualityComparer.Default.Equals(field, value)) return false; - field = value; - OnPropertyChanged(propertyName); - return true; - } - public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); // Violates rule + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { - foreach (var disposable in _disposables) + var arrayPool = ArrayPool.Shared; + var disposablesCount = _disposables.Count; + var disposables = arrayPool.Rent(disposablesCount); + _disposables.CopyTo(disposables); + for (var i = 0; i < disposablesCount; i++) { - disposable.Dispose(); + disposables[i].Dispose(); } + arrayPool.Return(disposables, true); + _disposables.Clear(); + Disposed?.Invoke(this); } } } \ No newline at end of file diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs new file mode 100644 index 0000000..6199d5b --- /dev/null +++ b/src/Library/TerminalUI/EventLoop.cs @@ -0,0 +1,130 @@ +using System.Buffers; +using System.Collections.Concurrent; +using TerminalUI.Controls; + +namespace TerminalUI; + +public class EventLoop : IEventLoop +{ + private readonly IApplicationContext _applicationContext; + private readonly object _lock = new(); + private readonly ArrayPool _viewPool = ArrayPool.Shared; + + private readonly ConcurrentBag _viewsToRenderInstantly = new(); + private readonly LinkedList _viewsToRender = new(); + + public EventLoop(IApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + public void Run() + { + _applicationContext.IsRunning = true; + while (_applicationContext.IsRunning) + { + Render(); + Thread.Sleep(10); + } + } + + public void Render() + { + IView[]? viewsToRenderCopy = null; + IView[]? viewsAlreadyRendered = null; + try + { + int viewsToRenderCopyCount; + IView[]? viewsToRenderInstantly; + + lock (_lock) + { + CleanViewsToRender(); + + viewsToRenderCopyCount = _viewsToRender.Count; + viewsToRenderCopy = _viewPool.Rent(_viewsToRender.Count); + _viewsToRender.CopyTo(viewsToRenderCopy, 0); + + viewsToRenderInstantly = _viewsToRenderInstantly.ToArray(); + _viewsToRenderInstantly.Clear(); + } + + viewsAlreadyRendered = _viewPool.Rent(viewsToRenderCopy.Length + viewsToRenderInstantly.Length); + var viewsAlreadyRenderedIndex = 0; + + foreach (var view in viewsToRenderInstantly) + { + if (Contains(viewsAlreadyRendered, view, viewsAlreadyRenderedIndex)) continue; + + view.Render(); + viewsAlreadyRendered[viewsAlreadyRenderedIndex++] = view; + } + + for (var i = 0; i < viewsToRenderCopyCount; i++) + { + var view = viewsToRenderCopy[i]; + if (Contains(viewsAlreadyRendered, view, viewsAlreadyRenderedIndex)) continue; + + view.Render(); + viewsAlreadyRendered[viewsAlreadyRenderedIndex++] = view; + } + } + finally + { + if (viewsToRenderCopy is not null) + _viewPool.Return(viewsToRenderCopy); + + if (viewsAlreadyRendered is not null) + _viewPool.Return(viewsAlreadyRendered); + } + } + + private void CleanViewsToRender() + { + IView[]? viewsAlreadyProcessed = null; + try + { + viewsAlreadyProcessed = _viewPool.Rent(_viewsToRender.Count); + var viewsAlreadyProcessedIndex = 0; + + var currentItem = _viewsToRender.First; + for (var i = 0; i < _viewsToRender.Count && currentItem is not null; i++) + { + if (Contains(viewsAlreadyProcessed, currentItem.Value, viewsAlreadyProcessedIndex)) + { + var itemToRemove = currentItem; + currentItem = currentItem.Next; + _viewsToRender.Remove(itemToRemove); + continue; + } + + viewsAlreadyProcessed[viewsAlreadyProcessedIndex++] = currentItem.Value; + } + } + finally + { + if (viewsAlreadyProcessed is not null) + { + _viewPool.Return(viewsAlreadyProcessed); + } + } + } + + private static bool Contains(IView[] views, IView view, int max) + { + for (var i = 0; i < max; i++) + { + if (views[i] == view) return true; + } + + return false; + } + + public void AddViewToRender(IView view) + { + lock (_lock) + { + _viewsToRender.AddLast(view); + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/Binding.cs index dd2d7f9..ad5e74b 100644 --- a/src/Library/TerminalUI/Extensions/Binding.cs +++ b/src/Library/TerminalUI/Extensions/Binding.cs @@ -6,18 +6,22 @@ namespace TerminalUI.Extensions; public static class Binding { - public static Binding Bind( + public static Binding Bind( this TView targetView, - IView view, - Expression> dataContextExpression, - Expression> propertyExpression) + IView dataSourceView, + Expression> dataContextExpression, + Expression> propertyExpression, + IEnumerable? rerenderProperties = null) { - var dataContextMapper = dataContextExpression.Compile(); - - if (propertyExpression.Body is not MemberExpression memberExpression - || memberExpression.Member is not PropertyInfo propertyInfo) + if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo}) throw new AggregateException(nameof(propertyExpression) + " must be a property expression"); - return new Binding(view, dataContextMapper, targetView, propertyInfo); + return new Binding( + dataSourceView, + dataContextExpression, + targetView, + propertyInfo, + rerenderProperties + ); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs new file mode 100644 index 0000000..fcc23f3 --- /dev/null +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -0,0 +1,7 @@ +namespace TerminalUI; + +public interface IApplicationContext +{ + IEventLoop EventLoop { get; init; } + bool IsRunning { get; set; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/IEventLoop.cs b/src/Library/TerminalUI/IEventLoop.cs new file mode 100644 index 0000000..91a39d5 --- /dev/null +++ b/src/Library/TerminalUI/IEventLoop.cs @@ -0,0 +1,10 @@ +using TerminalUI.Controls; + +namespace TerminalUI; + +public interface IEventLoop +{ + void Render(); + void AddViewToRender(IView view); + void Run(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/TerminalUI.csproj b/src/Library/TerminalUI/TerminalUI.csproj index 778f1f3..c5e3ac5 100644 --- a/src/Library/TerminalUI/TerminalUI.csproj +++ b/src/Library/TerminalUI/TerminalUI.csproj @@ -10,4 +10,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Library/TerminalUI/Traits/IContentRenderer.cs b/src/Library/TerminalUI/Traits/IContentRenderer.cs new file mode 100644 index 0000000..6c8106a --- /dev/null +++ b/src/Library/TerminalUI/Traits/IContentRenderer.cs @@ -0,0 +1,9 @@ +using TerminalUI.Controls; + +namespace TerminalUI.Traits; + +public interface IContentRenderer +{ + IView? Content { get; set; } + Action ContentRendererMethod { get; set; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Traits/IDisposableCollection.cs b/src/Library/TerminalUI/Traits/IDisposableCollection.cs index df8d761..d236b1d 100644 --- a/src/Library/TerminalUI/Traits/IDisposableCollection.cs +++ b/src/Library/TerminalUI/Traits/IDisposableCollection.cs @@ -3,4 +3,5 @@ public interface IDisposableCollection : IDisposable { void AddDisposable(IDisposable disposable); + void RemoveDisposable(IDisposable disposable); } \ No newline at end of file