EventLoop V1
This commit is contained in:
@@ -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<Key> _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<Key> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<IAppState, IItemViewModel> selectedItemsView = new();
|
||||
ListView<IAppState, IItemViewModel> selectedItemsView = new()
|
||||
{
|
||||
ApplicationContext = _applicationContext
|
||||
};
|
||||
selectedItemsView.DataContext = _consoleAppState;
|
||||
selectedItemsView.ItemTemplate = item =>
|
||||
{
|
||||
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<IKeyInputHandlerService, KeyInputHandlerService>();
|
||||
services.AddSingleton<CustomLoggerSink>();
|
||||
|
||||
services.TryAddSingleton<IApplicationContext, ApplicationContext>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\..\Library\CircularBuffer\CircularBuffer.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Defer\Defer.csproj" />
|
||||
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
||||
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
12
src/Library/TerminalUI/ApplicationContext.cs
Normal file
12
src/Library/TerminalUI/ApplicationContext.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<TDataContext, TResult> : IDisposable
|
||||
{
|
||||
private readonly Func<TDataContext, TResult> _dataContextMapper;
|
||||
private IView<TDataContext> _view;
|
||||
private IView<TDataContext> _dataSourceView;
|
||||
private object? _propertySource;
|
||||
private PropertyInfo _targetProperty;
|
||||
private readonly List<string> _rerenderProperties;
|
||||
private readonly IDisposableCollection? _propertySourceDisposableCollection;
|
||||
private INotifyPropertyChanged? _dataSourceLastDataContext;
|
||||
|
||||
public Binding(
|
||||
IView<TDataContext> view,
|
||||
Func<TDataContext, TResult> dataContextMapper,
|
||||
IView<TDataContext> dataSourceView,
|
||||
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||
object? propertySource,
|
||||
PropertyInfo targetProperty
|
||||
PropertyInfo targetProperty,
|
||||
IEnumerable<string>? 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));
|
||||
_rerenderProperties = rerenderProperties?.ToList() ?? new List<string>();
|
||||
|
||||
if(propertySource is IDisposableCollection disposableCollection)
|
||||
disposableCollection.AddDisposable(this);
|
||||
FindReactiveProperties(dataContextExpression);
|
||||
|
||||
view.AddDisposable(this);
|
||||
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<TDataContext>.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!;
|
||||
}
|
||||
|
||||
15
src/Library/TerminalUI/Controls/ContentView.cs
Normal file
15
src/Library/TerminalUI/Controls/ContentView.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract class ContentView<T>: View<T>, IContentRenderer
|
||||
{
|
||||
protected ContentView()
|
||||
{
|
||||
ContentRendererMethod = DefaultContentRender;
|
||||
}
|
||||
public IView? Content { get; set; }
|
||||
public Action ContentRendererMethod { get; set; }
|
||||
|
||||
private void DefaultContentRender() => Content?.Render();
|
||||
}
|
||||
@@ -3,10 +3,26 @@ using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public interface IView<T> : 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<IView> Disposed;
|
||||
event Action<IView> RenderRequested;
|
||||
void Render();
|
||||
void RequestRerender();
|
||||
}
|
||||
|
||||
public interface IView<T> : IView
|
||||
{
|
||||
new T? DataContext { get; set; }
|
||||
|
||||
object? IView.DataContext
|
||||
{
|
||||
get => DataContext;
|
||||
set => DataContext = (T?) value;
|
||||
}
|
||||
|
||||
TChild CreateChild<TChild>()
|
||||
where TChild : IView<T>, new();
|
||||
|
||||
@@ -52,12 +52,31 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render()
|
||||
public Func<ListViewItem<TItem>, 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<ListViewItem<TItem>> InstantiateItemViews()
|
||||
{
|
||||
if (_getItems is null)
|
||||
{
|
||||
if (_listViewItemLength != 0)
|
||||
{
|
||||
return InstantiateEmptyItemViews();
|
||||
}
|
||||
|
||||
return _listViewItems;
|
||||
}
|
||||
var items = _getItems().ToList();
|
||||
|
||||
Span<ListViewItem<TItem>> listViewItems = null;
|
||||
Span<ListViewItem<TItem>> listViewItems;
|
||||
|
||||
if (_listViewItems is null || _listViewItems.Length != items.Count)
|
||||
{
|
||||
@@ -65,7 +84,10 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var dataContext = items[i];
|
||||
newListViewItems[i] = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
|
||||
var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
|
||||
child.Content = ItemTemplate(child);
|
||||
ItemTemplate(child);
|
||||
newListViewItems[i] = child;
|
||||
}
|
||||
|
||||
_listViewItems = newListViewItems;
|
||||
@@ -77,9 +99,15 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
listViewItems = _listViewItems[.._listViewItemLength];
|
||||
}
|
||||
|
||||
foreach (var item in listViewItems)
|
||||
return listViewItems;
|
||||
}
|
||||
|
||||
private Span<ListViewItem<TItem>> InstantiateEmptyItemViews()
|
||||
{
|
||||
item.Render();
|
||||
}
|
||||
_listViewItems = ListViewItemPool.Rent(0);
|
||||
_listViewItemLength = 0;
|
||||
return _listViewItems;
|
||||
}
|
||||
|
||||
private static IView? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null;
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
namespace TerminalUI.Controls;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
public class ListViewItem<T> : View<T>
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public class ListViewItem<T> : ContentView<T>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
23
src/Library/TerminalUI/Controls/TextBlock.cs
Normal file
23
src/Library/TerminalUI/Controls/TextBlock.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using TerminalUI.Extensions;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public partial class TextBlock<T> : View<T>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
@@ -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<T> : IView<T>
|
||||
public abstract partial class View<T> : IView<T>
|
||||
{
|
||||
private readonly ConcurrentBag<IDisposable> _disposables = new();
|
||||
public T? DataContext { get; set; }
|
||||
public abstract void Render();
|
||||
private readonly List<IDisposable> _disposables = new();
|
||||
[Notify] private T? _dataContext;
|
||||
public Action RenderMethod { get; set; }
|
||||
public IApplicationContext ApplicationContext { get; init; }
|
||||
public event Action<IView>? Disposed;
|
||||
public event Action<IView>? RenderRequested;
|
||||
protected List<string> 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<TChild>() where TChild : IView<T>, new()
|
||||
{
|
||||
var child = new TChild
|
||||
{
|
||||
DataContext = DataContext
|
||||
DataContext = DataContext,
|
||||
ApplicationContext = ApplicationContext
|
||||
};
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
|
||||
AddDisposable(mapper);
|
||||
@@ -28,7 +71,8 @@ public abstract class View<T> : IView<T>
|
||||
{
|
||||
var child = new TChild
|
||||
{
|
||||
DataContext = dataContextMapper(DataContext)
|
||||
DataContext = dataContextMapper(DataContext),
|
||||
ApplicationContext = ApplicationContext
|
||||
};
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
|
||||
AddDisposable(mapper);
|
||||
@@ -38,36 +82,36 @@ public abstract class View<T> : IView<T>
|
||||
}
|
||||
|
||||
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<TProp>(ref TProp field, TProp value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<TProp>.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<IDisposable>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/Library/TerminalUI/EventLoop.cs
Normal file
130
src/Library/TerminalUI/EventLoop.cs
Normal file
@@ -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<IView> _viewPool = ArrayPool<IView>.Shared;
|
||||
|
||||
private readonly ConcurrentBag<IView> _viewsToRenderInstantly = new();
|
||||
private readonly LinkedList<IView> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,22 @@ namespace TerminalUI.Extensions;
|
||||
|
||||
public static class Binding
|
||||
{
|
||||
public static Binding<TDataContext, TResult> Bind<TDataContext, TResult, TView>(
|
||||
public static Binding<TDataContext, TResult> Bind<TView, TDataContext, TResult>(
|
||||
this TView targetView,
|
||||
IView<TDataContext> view,
|
||||
Expression<Func<TDataContext, TResult>> dataContextExpression,
|
||||
Expression<Func<TView, TResult>> propertyExpression)
|
||||
IView<TDataContext> dataSourceView,
|
||||
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||
Expression<Func<TView, TResult>> propertyExpression,
|
||||
IEnumerable<string>? 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<TDataContext, TResult>(view, dataContextMapper, targetView, propertyInfo);
|
||||
return new Binding<TDataContext, TResult>(
|
||||
dataSourceView,
|
||||
dataContextExpression,
|
||||
targetView,
|
||||
propertyInfo,
|
||||
rerenderProperties
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/Library/TerminalUI/IApplicationContext.cs
Normal file
7
src/Library/TerminalUI/IApplicationContext.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace TerminalUI;
|
||||
|
||||
public interface IApplicationContext
|
||||
{
|
||||
IEventLoop EventLoop { get; init; }
|
||||
bool IsRunning { get; set; }
|
||||
}
|
||||
10
src/Library/TerminalUI/IEventLoop.cs
Normal file
10
src/Library/TerminalUI/IEventLoop.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public interface IEventLoop
|
||||
{
|
||||
void Render();
|
||||
void AddViewToRender(IView view);
|
||||
void Run();
|
||||
}
|
||||
@@ -10,4 +10,11 @@
|
||||
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
9
src/Library/TerminalUI/Traits/IContentRenderer.cs
Normal file
9
src/Library/TerminalUI/Traits/IContentRenderer.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI.Traits;
|
||||
|
||||
public interface IContentRenderer
|
||||
{
|
||||
IView? Content { get; set; }
|
||||
Action ContentRendererMethod { get; set; }
|
||||
}
|
||||
@@ -3,4 +3,5 @@
|
||||
public interface IDisposableCollection : IDisposable
|
||||
{
|
||||
void AddDisposable(IDisposable disposable);
|
||||
void RemoveDisposable(IDisposable disposable);
|
||||
}
|
||||
Reference in New Issue
Block a user