EventLoop V1
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using FileTime.App.Core.Services;
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.ConsoleUI.App.KeyInputHandling;
|
using FileTime.ConsoleUI.App.KeyInputHandling;
|
||||||
|
using TerminalUI;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ public class App : IApplication
|
|||||||
private readonly IConsoleAppState _consoleAppState;
|
private readonly IConsoleAppState _consoleAppState;
|
||||||
//private readonly IAppKeyService<Key> _appKeyService;
|
//private readonly IAppKeyService<Key> _appKeyService;
|
||||||
private readonly MainWindow _mainWindow;
|
private readonly MainWindow _mainWindow;
|
||||||
|
private readonly IApplicationContext _applicationContext;
|
||||||
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
||||||
|
|
||||||
public App(
|
public App(
|
||||||
@@ -16,20 +18,23 @@ public class App : IApplication
|
|||||||
IKeyInputHandlerService keyInputHandlerService,
|
IKeyInputHandlerService keyInputHandlerService,
|
||||||
IConsoleAppState consoleAppState,
|
IConsoleAppState consoleAppState,
|
||||||
//IAppKeyService<Key> appKeyService,
|
//IAppKeyService<Key> appKeyService,
|
||||||
MainWindow mainWindow)
|
MainWindow mainWindow,
|
||||||
|
IApplicationContext applicationContext)
|
||||||
{
|
{
|
||||||
_lifecycleService = lifecycleService;
|
_lifecycleService = lifecycleService;
|
||||||
_keyInputHandlerService = keyInputHandlerService;
|
_keyInputHandlerService = keyInputHandlerService;
|
||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
//_appKeyService = appKeyService;
|
//_appKeyService = appKeyService;
|
||||||
_mainWindow = mainWindow;
|
_mainWindow = mainWindow;
|
||||||
|
_applicationContext = applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run()
|
public void Run()
|
||||||
{
|
{
|
||||||
Console.WriteLine("Loading...");
|
|
||||||
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
||||||
|
|
||||||
_mainWindow.Initialize();
|
_mainWindow.Initialize();
|
||||||
|
|
||||||
|
_applicationContext.EventLoop.Run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Linq.Expressions;
|
||||||
using DeclarativeProperty;
|
using DeclarativeProperty;
|
||||||
using FileTime.App.Core.ViewModels;
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.ConsoleUI.App.Extensions;
|
|
||||||
using TerminalUI;
|
using TerminalUI;
|
||||||
using TerminalUI.Controls;
|
using TerminalUI.Controls;
|
||||||
using TerminalUI.Extensions;
|
using TerminalUI.Extensions;
|
||||||
@@ -11,23 +10,39 @@ namespace FileTime.ConsoleUI.App;
|
|||||||
public class MainWindow
|
public class MainWindow
|
||||||
{
|
{
|
||||||
private readonly IConsoleAppState _consoleAppState;
|
private readonly IConsoleAppState _consoleAppState;
|
||||||
|
private readonly IApplicationContext _applicationContext;
|
||||||
private const int ParentColumnWidth = 20;
|
private const int ParentColumnWidth = 20;
|
||||||
|
|
||||||
public MainWindow(IConsoleAppState consoleAppState)
|
public MainWindow(IConsoleAppState consoleAppState, IApplicationContext applicationContext)
|
||||||
{
|
{
|
||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
|
_applicationContext = applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
ListView<IAppState, IItemViewModel> selectedItemsView = new();
|
ListView<IAppState, IItemViewModel> selectedItemsView = new()
|
||||||
|
{
|
||||||
|
ApplicationContext = _applicationContext
|
||||||
|
};
|
||||||
selectedItemsView.DataContext = _consoleAppState;
|
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.Bind(
|
||||||
selectedItemsView,
|
selectedItemsView,
|
||||||
appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
|
appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
|
||||||
v => v.ItemsSource);
|
v => v.ItemsSource);
|
||||||
|
|
||||||
selectedItemsView.Render();
|
selectedItemsView.RequestRerender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using FileTime.ConsoleUI.App.Services;
|
|||||||
using FileTime.Core.Interactions;
|
using FileTime.Core.Interactions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using TerminalUI;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public static class Startup
|
|||||||
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
||||||
services.AddSingleton<CustomLoggerSink>();
|
services.AddSingleton<CustomLoggerSink>();
|
||||||
|
|
||||||
|
services.TryAddSingleton<IApplicationContext, ApplicationContext>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
<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="..\..\Library\Defer\Defer.csproj" />
|
||||||
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />
|
||||||
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
|
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using CircularBuffer;
|
||||||
using DeclarativeProperty;
|
using DeclarativeProperty;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using FileTime.App.Core.Services;
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.Core.Collections;
|
|
||||||
using FileTime.Core.Helper;
|
using FileTime.Core.Helper;
|
||||||
using FileTime.Core.Models;
|
using FileTime.Core.Models;
|
||||||
using FileTime.Core.Timeline;
|
using FileTime.Core.Timeline;
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.App.Abst
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\CircularBuffer\CircularBuffer.csproj", "{AF4FE804-12D9-46E2-A584-BFF6D4509766}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -376,6 +382,7 @@ Global
|
|||||||
{826AFD32-E36B-48BA-BC1E-1476B393CF24} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
{826AFD32-E36B-48BA-BC1E-1476B393CF24} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
||||||
{81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
|
{81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
|
||||||
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
|
{AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
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.ComponentModel;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using TerminalUI.Controls;
|
using TerminalUI.Controls;
|
||||||
using TerminalUI.Traits;
|
using TerminalUI.Traits;
|
||||||
@@ -8,41 +9,103 @@ namespace TerminalUI;
|
|||||||
public class Binding<TDataContext, TResult> : IDisposable
|
public class Binding<TDataContext, TResult> : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Func<TDataContext, TResult> _dataContextMapper;
|
private readonly Func<TDataContext, TResult> _dataContextMapper;
|
||||||
private IView<TDataContext> _view;
|
private IView<TDataContext> _dataSourceView;
|
||||||
private object? _propertySource;
|
private object? _propertySource;
|
||||||
private PropertyInfo _targetProperty;
|
private PropertyInfo _targetProperty;
|
||||||
|
private readonly List<string> _rerenderProperties;
|
||||||
|
private readonly IDisposableCollection? _propertySourceDisposableCollection;
|
||||||
|
private INotifyPropertyChanged? _dataSourceLastDataContext;
|
||||||
|
|
||||||
public Binding(
|
public Binding(
|
||||||
IView<TDataContext> view,
|
IView<TDataContext> dataSourceView,
|
||||||
Func<TDataContext, TResult> dataContextMapper,
|
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||||
object? propertySource,
|
object? propertySource,
|
||||||
PropertyInfo targetProperty
|
PropertyInfo targetProperty,
|
||||||
|
IEnumerable<string>? rerenderProperties = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_view = view;
|
ArgumentNullException.ThrowIfNull(dataSourceView);
|
||||||
_dataContextMapper = dataContextMapper;
|
ArgumentNullException.ThrowIfNull(dataContextExpression);
|
||||||
|
ArgumentNullException.ThrowIfNull(targetProperty);
|
||||||
|
_dataSourceView = dataSourceView;
|
||||||
|
_dataContextMapper = dataContextExpression.Compile();
|
||||||
_propertySource = propertySource;
|
_propertySource = propertySource;
|
||||||
_targetProperty = targetProperty;
|
_targetProperty = targetProperty;
|
||||||
view.PropertyChanged += View_PropertyChanged;
|
_rerenderProperties = rerenderProperties?.ToList() ?? new List<string>();
|
||||||
_targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext));
|
|
||||||
|
|
||||||
if(propertySource is IDisposableCollection disposableCollection)
|
FindReactiveProperties(dataContextExpression);
|
||||||
disposableCollection.AddDisposable(this);
|
|
||||||
|
|
||||||
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)
|
private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_view.PropertyChanged -= View_PropertyChanged;
|
_propertySourceDisposableCollection?.RemoveDisposable(this);
|
||||||
_view = null!;
|
_dataSourceView.RemoveDisposable(this);
|
||||||
|
_dataSourceView.PropertyChanged -= View_PropertyChanged;
|
||||||
|
_dataSourceView = null!;
|
||||||
_propertySource = null!;
|
_propertySource = null!;
|
||||||
_targetProperty = 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;
|
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 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>()
|
TChild CreateChild<TChild>()
|
||||||
where TChild : IView<T>, new();
|
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();
|
var items = _getItems().ToList();
|
||||||
|
|
||||||
Span<ListViewItem<TItem>> listViewItems = null;
|
Span<ListViewItem<TItem>> listViewItems;
|
||||||
|
|
||||||
if (_listViewItems is null || _listViewItems.Length != items.Count)
|
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++)
|
for (var i = 0; i < items.Count; i++)
|
||||||
{
|
{
|
||||||
var dataContext = items[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;
|
_listViewItems = newListViewItems;
|
||||||
@@ -77,9 +99,15 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
|
|||||||
listViewItems = _listViewItems[.._listViewItemLength];
|
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.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|
||||||
namespace TerminalUI.Controls;
|
namespace TerminalUI.Controls;
|
||||||
|
|
||||||
public abstract class View<T> : IView<T>
|
public abstract partial class View<T> : IView<T>
|
||||||
{
|
{
|
||||||
private readonly ConcurrentBag<IDisposable> _disposables = new();
|
private readonly List<IDisposable> _disposables = new();
|
||||||
public T? DataContext { get; set; }
|
[Notify] private T? _dataContext;
|
||||||
public abstract void Render();
|
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()
|
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
|
||||||
{
|
{
|
||||||
var child = new TChild
|
var child = new TChild
|
||||||
{
|
{
|
||||||
DataContext = DataContext
|
DataContext = DataContext,
|
||||||
|
ApplicationContext = ApplicationContext
|
||||||
};
|
};
|
||||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
|
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
|
||||||
AddDisposable(mapper);
|
AddDisposable(mapper);
|
||||||
@@ -28,7 +71,8 @@ public abstract class View<T> : IView<T>
|
|||||||
{
|
{
|
||||||
var child = new TChild
|
var child = new TChild
|
||||||
{
|
{
|
||||||
DataContext = dataContextMapper(DataContext)
|
DataContext = dataContextMapper(DataContext),
|
||||||
|
ApplicationContext = ApplicationContext
|
||||||
};
|
};
|
||||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
|
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
|
||||||
AddDisposable(mapper);
|
AddDisposable(mapper);
|
||||||
@@ -38,36 +82,36 @@ public abstract class View<T> : IView<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
||||||
|
public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable);
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
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));
|
=> 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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
GC.SuppressFinalize(this); // Violates rule
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (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();
|
_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 class Binding
|
||||||
{
|
{
|
||||||
public static Binding<TDataContext, TResult> Bind<TDataContext, TResult, TView>(
|
public static Binding<TDataContext, TResult> Bind<TView, TDataContext, TResult>(
|
||||||
this TView targetView,
|
this TView targetView,
|
||||||
IView<TDataContext> view,
|
IView<TDataContext> dataSourceView,
|
||||||
Expression<Func<TDataContext, TResult>> dataContextExpression,
|
Expression<Func<TDataContext?, TResult>> dataContextExpression,
|
||||||
Expression<Func<TView, TResult>> propertyExpression)
|
Expression<Func<TView, TResult>> propertyExpression,
|
||||||
|
IEnumerable<string>? rerenderProperties = null)
|
||||||
{
|
{
|
||||||
var dataContextMapper = dataContextExpression.Compile();
|
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
|
||||||
|
|
||||||
if (propertyExpression.Body is not MemberExpression memberExpression
|
|
||||||
|| memberExpression.Member is not PropertyInfo propertyInfo)
|
|
||||||
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
|
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" />
|
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
public interface IDisposableCollection : IDisposable
|
||||||
{
|
{
|
||||||
void AddDisposable(IDisposable disposable);
|
void AddDisposable(IDisposable disposable);
|
||||||
|
void RemoveDisposable(IDisposable disposable);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user