Custom TUI Library WIP
This commit is contained in:
@@ -91,6 +91,6 @@ public static class DeclarativePropertyExtensions
|
||||
Action<TResult?>? setValueHook = null)
|
||||
=> new CombineLatestProperty<T1,T2,TResult?>(prop1, prop2, func, setValueHook);
|
||||
|
||||
public static IDeclarativeProperty<T?> Switch<T>(this IDeclarativeProperty<IDeclarativeProperty<T>> from)
|
||||
public static IDeclarativeProperty<T> Switch<T>(this IDeclarativeProperty<IDeclarativeProperty<T>?> from)
|
||||
=> new SwitchProperty<T>(from);
|
||||
}
|
||||
@@ -4,13 +4,13 @@ public sealed class SwitchProperty<TItem> : DeclarativePropertyBase<TItem>
|
||||
{
|
||||
private IDisposable? _innerSubscription;
|
||||
|
||||
public SwitchProperty(IDeclarativeProperty<IDeclarativeProperty<TItem?>?> from) : base(from.Value is null ? default : from.Value.Value)
|
||||
public SwitchProperty(IDeclarativeProperty<IDeclarativeProperty<TItem>?> from) : base(from.Value is null ? default : from.Value.Value)
|
||||
{
|
||||
AddDisposable(from.Subscribe(HandleStreamChange));
|
||||
_innerSubscription = from.Value?.Subscribe(HandleInnerValueChange);
|
||||
}
|
||||
|
||||
private async Task HandleStreamChange(IDeclarativeProperty<TItem?>? next, CancellationToken token)
|
||||
private async Task HandleStreamChange(IDeclarativeProperty<TItem>? next, CancellationToken token)
|
||||
{
|
||||
_innerSubscription?.Dispose();
|
||||
_innerSubscription = next?.Subscribe(HandleInnerValueChange);
|
||||
|
||||
49
src/Library/TerminalUI/Binding.cs
Normal file
49
src/Library/TerminalUI/Binding.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using TerminalUI.Controls;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class Binding<TDataContext, TResult> : IDisposable
|
||||
{
|
||||
private readonly Func<TDataContext, TResult> _dataContextMapper;
|
||||
private IView<TDataContext> _view;
|
||||
private object? _propertySource;
|
||||
private PropertyInfo _targetProperty;
|
||||
|
||||
public Binding(
|
||||
IView<TDataContext> view,
|
||||
Func<TDataContext, TResult> dataContextMapper,
|
||||
object? propertySource,
|
||||
PropertyInfo targetProperty
|
||||
)
|
||||
{
|
||||
_view = view;
|
||||
_dataContextMapper = dataContextMapper;
|
||||
_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);
|
||||
}
|
||||
|
||||
private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
||||
|
||||
_targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_view.PropertyChanged -= View_PropertyChanged;
|
||||
_view = null!;
|
||||
_propertySource = null!;
|
||||
_targetProperty = null!;
|
||||
}
|
||||
}
|
||||
16
src/Library/TerminalUI/Controls/IView.cs
Normal file
16
src/Library/TerminalUI/Controls/IView.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public interface IView<T> : INotifyPropertyChanged, IDisposableCollection
|
||||
{
|
||||
T? DataContext { get; set; }
|
||||
void Render();
|
||||
|
||||
TChild CreateChild<TChild>()
|
||||
where TChild : IView<T>, new();
|
||||
|
||||
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
|
||||
where TChild : IView<TDataContext>, new();
|
||||
}
|
||||
85
src/Library/TerminalUI/Controls/ListView.cs
Normal file
85
src/Library/TerminalUI/Controls/ListView.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using DeclarativeProperty;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public class ListView<TDataContext, TItem> : View<TDataContext>
|
||||
{
|
||||
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
|
||||
|
||||
private readonly List<IDisposable> _itemsDisposables = new();
|
||||
private Func<IEnumerable<TItem>>? _getItems;
|
||||
private object? _itemsSource;
|
||||
private ListViewItem<TItem>[]? _listViewItems;
|
||||
private int _listViewItemLength;
|
||||
|
||||
public object? ItemsSource
|
||||
{
|
||||
get => _itemsSource;
|
||||
set
|
||||
{
|
||||
if (_itemsSource == value) return;
|
||||
_itemsSource = value;
|
||||
|
||||
foreach (var disposable in _itemsDisposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_itemsDisposables.Clear();
|
||||
|
||||
if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty)
|
||||
_getItems = () => observableDeclarativeProperty.Value;
|
||||
else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty)
|
||||
_getItems = () => readOnlyObservableDeclarativeProperty.Value;
|
||||
else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty)
|
||||
_getItems = () => enumerableDeclarativeProperty.Value;
|
||||
else if (_itemsSource is ICollection<TItem> collection)
|
||||
_getItems = () => collection;
|
||||
else if (_itemsSource is TItem[] array)
|
||||
_getItems = () => array;
|
||||
else if (_itemsSource is IEnumerable<TItem> enumerable)
|
||||
_getItems = () => enumerable.ToArray();
|
||||
|
||||
if (_listViewItems is not null)
|
||||
{
|
||||
ListViewItemPool.Return(_listViewItems);
|
||||
_listViewItems = null;
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render()
|
||||
{
|
||||
if (_getItems is null) return;
|
||||
var items = _getItems().ToList();
|
||||
|
||||
Span<ListViewItem<TItem>> listViewItems = null;
|
||||
|
||||
if (_listViewItems is null || _listViewItems.Length != items.Count)
|
||||
{
|
||||
var newListViewItems = ListViewItemPool.Rent(items.Count);
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var dataContext = items[i];
|
||||
newListViewItems[i] = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
|
||||
}
|
||||
|
||||
_listViewItems = newListViewItems;
|
||||
_listViewItemLength = items.Count;
|
||||
listViewItems = newListViewItems[..items.Count];
|
||||
}
|
||||
else
|
||||
{
|
||||
listViewItems = _listViewItems[.._listViewItemLength];
|
||||
}
|
||||
|
||||
foreach (var item in listViewItems)
|
||||
{
|
||||
item.Render();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Library/TerminalUI/Controls/ListViewItem.cs
Normal file
9
src/Library/TerminalUI/Controls/ListViewItem.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public class ListViewItem<T> : View<T>
|
||||
{
|
||||
public override void Render()
|
||||
{
|
||||
Console.WriteLine(DataContext?.ToString());
|
||||
}
|
||||
}
|
||||
73
src/Library/TerminalUI/Controls/View.cs
Normal file
73
src/Library/TerminalUI/Controls/View.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace TerminalUI.Controls;
|
||||
|
||||
public abstract class View<T> : IView<T>
|
||||
{
|
||||
private readonly ConcurrentBag<IDisposable> _disposables = new();
|
||||
public T? DataContext { get; set; }
|
||||
public abstract void Render();
|
||||
|
||||
public TChild CreateChild<TChild>() where TChild : IView<T>, new()
|
||||
{
|
||||
var child = new TChild
|
||||
{
|
||||
DataContext = DataContext
|
||||
};
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d);
|
||||
AddDisposable(mapper);
|
||||
child.AddDisposable(mapper);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
public TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
|
||||
where TChild : IView<TDataContext>, new()
|
||||
{
|
||||
var child = new TChild
|
||||
{
|
||||
DataContext = dataContextMapper(DataContext)
|
||||
};
|
||||
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d));
|
||||
AddDisposable(mapper);
|
||||
child.AddDisposable(mapper);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual 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
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var disposable in _disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_disposables.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Library/TerminalUI/DataContextMapper.cs
Normal file
27
src/Library/TerminalUI/DataContextMapper.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class DataContextMapper<T> : IDisposable
|
||||
{
|
||||
private readonly IView<T> _source;
|
||||
private readonly Action<T?> _setter;
|
||||
|
||||
public DataContextMapper(IView<T> source, Action<T?> setter)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
_source = source;
|
||||
_setter = setter;
|
||||
source.PropertyChanged += SourceOnPropertyChanged;
|
||||
}
|
||||
|
||||
private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(IView<object>.DataContext)) return;
|
||||
_setter(_source.DataContext);
|
||||
}
|
||||
|
||||
public void Dispose() => _source.PropertyChanged -= SourceOnPropertyChanged;
|
||||
}
|
||||
23
src/Library/TerminalUI/Extensions/Binding.cs
Normal file
23
src/Library/TerminalUI/Extensions/Binding.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI.Extensions;
|
||||
|
||||
public static class Binding
|
||||
{
|
||||
public static Binding<TDataContext, TResult> Bind<TDataContext, TResult, TView>(
|
||||
this TView targetView,
|
||||
IView<TDataContext> view,
|
||||
Expression<Func<TDataContext, TResult>> dataContextExpression,
|
||||
Expression<Func<TView, TResult>> propertyExpression)
|
||||
{
|
||||
var dataContextMapper = dataContextExpression.Compile();
|
||||
|
||||
if (propertyExpression.Body is not MemberExpression memberExpression
|
||||
|| memberExpression.Member is not PropertyInfo propertyInfo)
|
||||
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
|
||||
|
||||
return new Binding<TDataContext, TResult>(view, dataContextMapper, targetView, propertyInfo);
|
||||
}
|
||||
}
|
||||
13
src/Library/TerminalUI/TerminalUI.csproj
Normal file
13
src/Library/TerminalUI/TerminalUI.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
src/Library/TerminalUI/Traits/IDisposableCollection.cs
Normal file
6
src/Library/TerminalUI/Traits/IDisposableCollection.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace TerminalUI.Traits;
|
||||
|
||||
public interface IDisposableCollection : IDisposable
|
||||
{
|
||||
void AddDisposable(IDisposable disposable);
|
||||
}
|
||||
Reference in New Issue
Block a user