diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj index 316f89f..22168c5 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index dee940b..e338921 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -1,8 +1,5 @@ using FileTime.App.Core.Services; -using FileTime.ConsoleUI.App.Extensions; using FileTime.ConsoleUI.App.KeyInputHandling; -using FileTime.Core.Models; -using Terminal.Gui; namespace FileTime.ConsoleUI.App; @@ -10,7 +7,7 @@ public class App : IApplication { private readonly ILifecycleService _lifecycleService; private readonly IConsoleAppState _consoleAppState; - private readonly IAppKeyService _appKeyService; + //private readonly IAppKeyService _appKeyService; private readonly MainWindow _mainWindow; private readonly IKeyInputHandlerService _keyInputHandlerService; @@ -18,13 +15,13 @@ public class App : IApplication ILifecycleService lifecycleService, IKeyInputHandlerService keyInputHandlerService, IConsoleAppState consoleAppState, - IAppKeyService appKeyService, + //IAppKeyService appKeyService, MainWindow mainWindow) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; _consoleAppState = consoleAppState; - _appKeyService = appKeyService; + //_appKeyService = appKeyService; _mainWindow = mainWindow; } @@ -34,24 +31,5 @@ public class App : IApplication Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait(); _mainWindow.Initialize(); - - Application.Init(); - var asd = Application.Top.ColorScheme; - - foreach (var element in _mainWindow.GetElements()) - { - Application.Top.Add(element); - } - - Application.RootKeyEvent += e => - { - if (e.ToGeneralKeyEventArgs(_appKeyService) is not { } args) return false; - _keyInputHandlerService.HandleKeyInput(args); - - return args.Handled; - }; - - Application.Run(); - Application.Shutdown(); } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs deleted file mode 100644 index 7620731..0000000 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemRenderer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections; -using System.Collections.ObjectModel; -using DeclarativeProperty; -using FileTime.App.Core.ViewModels; -using Terminal.Gui; - -namespace FileTime.ConsoleUI.App.Controls; - -public class ItemRenderer : IListDataSource -{ - private readonly IDeclarativeProperty?> _source; - - public ItemRenderer( - IDeclarativeProperty?> source, - Action update - ) - { - _source = source; - source.Subscribe((_, _) => - { - update(); - }); - } - - public void Render(ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0) - { - var itemViewModel = _source.Value?[item]; - container.Move(col, line); - driver.AddStr(itemViewModel?.DisplayNameText ?? string.Empty); - } - - public bool IsMarked(int item) => false; - - public void SetMark(int item, bool value) - { - } - - public IList ToList() => _source.Value!; - - public int Count => _source.Value?.Count ?? 0; - public int Length => 20; -} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs deleted file mode 100644 index ccdcdb3..0000000 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/KeyEventArgsExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FileTime.App.Core.Models; -using FileTime.App.Core.Services; -using Terminal.Gui; - -namespace FileTime.ConsoleUI.App.Extensions; - -public static class KeyEventArgsExtensions -{ - public static GeneralKeyEventArgs? ToGeneralKeyEventArgs(this KeyEvent args, IAppKeyService appKeyService) - { - var maybeKey = appKeyService.MapKey(args.Key); - if (maybeKey is not { } key1) return null; - return new GeneralKeyEventArgs - { - Key = key1 - }; - } -} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs deleted file mode 100644 index d25e2ca..0000000 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Extensions/UIExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Terminal.Gui; - -namespace FileTime.ConsoleUI.App.Extensions; - -public static class UiExtensions -{ - public static void Update(this ListView listView) - { - listView.EnsureSelectedItemVisible(); - listView.SetNeedsDisplay(); - } -} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj index 1cea262..93c5f85 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj @@ -13,12 +13,16 @@ - + + + + + diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs deleted file mode 100644 index dc0294f..0000000 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.ObjectModel; -using FileTime.App.Core.Models; -using FileTime.App.Core.Services; -using Terminal.Gui; - -namespace FileTime.ConsoleUI.App.KeyInputHandling; - -public sealed class ConsoleAppKeyService : IAppKeyService -{ - private static readonly Dictionary KeyMapping; - - //TODO: write test for this. Test if every enum value is present in the dictionary. - public static ReadOnlyDictionary KeyMappingReadOnly { get; } - - static ConsoleAppKeyService() - { - KeyMapping = new Dictionary - { - {Key.A, Keys.A}, - {Key.B, Keys.B}, - {Key.C, Keys.C}, - {Key.D, Keys.D}, - {Key.E, Keys.E}, - {Key.F, Keys.F}, - {Key.G, Keys.G}, - {Key.H, Keys.H}, - {Key.I, Keys.I}, - {Key.J, Keys.J}, - {Key.K, Keys.K}, - {Key.L, Keys.L}, - {Key.M, Keys.M}, - {Key.N, Keys.N}, - {Key.O, Keys.O}, - {Key.P, Keys.P}, - {Key.Q, Keys.Q}, - {Key.R, Keys.R}, - {Key.S, Keys.S}, - {Key.T, Keys.T}, - {Key.U, Keys.U}, - {Key.V, Keys.V}, - {Key.W, Keys.W}, - {Key.X, Keys.X}, - {Key.Y, Keys.Y}, - {Key.Z, Keys.Z}, - {Key.F1, Keys.F1}, - {Key.F2, Keys.F2}, - {Key.F3, Keys.F3}, - {Key.F4, Keys.F4}, - {Key.F5, Keys.F5}, - {Key.F6, Keys.F6}, - {Key.F7, Keys.F7}, - {Key.F8, Keys.F8}, - {Key.F9, Keys.F9}, - {Key.F10, Keys.F10}, - {Key.F11, Keys.F11}, - {Key.F12, Keys.F12}, - {Key.D0, Keys.Num0}, - {Key.D1, Keys.Num1}, - {Key.D2, Keys.Num2}, - {Key.D3, Keys.Num3}, - {Key.D4, Keys.Num4}, - {Key.D5, Keys.Num5}, - {Key.D6, Keys.Num6}, - {Key.D7, Keys.Num7}, - {Key.D8, Keys.Num8}, - {Key.D9, Keys.Num9}, - {Key.CursorUp, Keys.Up}, - {Key.CursorDown, Keys.Down}, - {Key.CursorLeft, Keys.Left}, - {Key.CursorRight, Keys.Right}, - {Key.Enter, Keys.Enter}, - {Key.Esc, Keys.Escape}, - {Key.Backspace, Keys.Backspace}, - {Key.Space, Keys.Space}, - {Key.PageUp, Keys.PageUp}, - {Key.PageDown, Keys.PageDown}, - {Key.Tab, Keys.Tab}, - {(Key)44, Keys.Comma}, - {(Key)63, Keys.Question}, - {(Key)123456, Keys.LWin}, - {(Key)123457, Keys.RWin}, - }; - - KeyMappingReadOnly = new(KeyMapping); - } - - public Keys? MapKey(Key key) - { - if (!KeyMapping.TryGetValue(key, out var mappedKey)) return null; - return mappedKey; - } -} \ 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 f3c2da1..302f38b 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,14 +1,16 @@ -using DeclarativeProperty; -using FileTime.ConsoleUI.App.Controls; +using System.Collections.ObjectModel; +using DeclarativeProperty; +using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.Extensions; -using Terminal.Gui; +using TerminalUI; +using TerminalUI.Controls; +using TerminalUI.Extensions; namespace FileTime.ConsoleUI.App; public class MainWindow { private readonly IConsoleAppState _consoleAppState; - private View[] _views; private const int ParentColumnWidth = 20; public MainWindow(IConsoleAppState consoleAppState) @@ -16,67 +18,16 @@ public class MainWindow _consoleAppState = consoleAppState; } - public void Initialize() => - _views = new View[] - { - GetSelectedItemsView(), - GetParentsChildren(), - GetSelectedsChildren() - }; - - private ListView GetSelectedItemsView() + public void Initialize() { - ListView selectedItemsView = new() {X = ParentColumnWidth, Y = 1, Width = Dim.Percent(60) - 20, Height = Dim.Fill()}; - var selectedsItems = _consoleAppState - .SelectedTab - .Map(t => t.CurrentItems) - .Switch(); + ListView selectedItemsView = new(); + selectedItemsView.DataContext = _consoleAppState; - var selectedItem = _consoleAppState.SelectedTab - .Map(t => t.CurrentSelectedItem) - .Switch(); - - DeclarativePropertyHelpers.CombineLatest( - selectedItem, - selectedsItems, - (selected, items) => Task.FromResult(items.IndexOf(selected))) - .Subscribe((index, _) => - { - if (index == -1) return; - selectedItemsView.SelectedItem = index; - selectedItemsView.Update(); - }); - - var renderer = new ItemRenderer(selectedsItems, selectedItemsView.Update); - selectedItemsView.Source = renderer; - return selectedItemsView; + selectedItemsView.Bind( + selectedItemsView, + appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), + v => v.ItemsSource); + + selectedItemsView.Render(); } - - private ListView GetParentsChildren() - { - ListView parentsChildrenView = new() {X = 0, Y = 1, Width = ParentColumnWidth, Height = Dim.Fill()}; - var parentsChildren = _consoleAppState - .SelectedTab - .Map(t => t.ParentsChildren) - .Switch(); - - var renderer = new ItemRenderer(parentsChildren, parentsChildrenView.Update); - parentsChildrenView.Source = renderer; - return parentsChildrenView; - } - - private ListView GetSelectedsChildren() - { - ListView selectedsChildrenView = new() {X = Pos.Percent(60), Y = 1, Width = Dim.Percent(40), Height = Dim.Fill()}; - var selectedsChildren = _consoleAppState - .SelectedTab - .Map(t => t.SelectedsChildren) - .Switch(); - - var renderer = new ItemRenderer(selectedsChildren, selectedsChildrenView.Update); - selectedsChildrenView.Source = renderer; - return selectedsChildrenView; - } - - public IEnumerable GetElements() => _views; } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs deleted file mode 100644 index d4777b1..0000000 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/SystemClipboardService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FileTime.App.Core.Services; -using FileTime.Core.Models; -using Terminal.Gui; - -namespace FileTime.ConsoleUI.App.Services; - -public class SystemClipboardService : ISystemClipboardService -{ - public Task CopyToClipboardAsync(string text) - { - Clipboard.TrySetClipboardData(text); - return Task.CompletedTask; - } - - public Task> GetFilesAsync() => throw new NotImplementedException(); - - public Task SetFilesAsync(IEnumerable files) => throw new NotImplementedException(); -} \ 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 1990aad..69fa664 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -5,7 +5,6 @@ using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Terminal.Gui; namespace FileTime.ConsoleUI.App; @@ -19,8 +18,6 @@ public static class Startup services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton, ConsoleAppKeyService>(); - services.TryAddSingleton(); services.AddSingleton(); return services; diff --git a/src/FileTime.sln b/src/FileTime.sln index 3305bbf..fff18c0 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -117,6 +117,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.ContainerSizeS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.App.Abstractions", "ConsoleApp\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj", "{81F44BBB-6F89-41B4-89F1-4A3204843DB5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -315,6 +317,10 @@ Global {81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Debug|Any CPU.Build.0 = Debug|Any CPU {81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Release|Any CPU.ActiveCfg = Release|Any CPU {81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Release|Any CPU.Build.0 = Release|Any CPU + {2F01FC4C-D942-48B0-B61C-7C5BEAED4787}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -369,6 +375,7 @@ Global {E5FD38ED-6E4B-42AA-850B-470B939B836B} = {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} + {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs index bf5aa8c..919afd5 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs @@ -91,6 +91,6 @@ public static class DeclarativePropertyExtensions Action? setValueHook = null) => new CombineLatestProperty(prop1, prop2, func, setValueHook); - public static IDeclarativeProperty Switch(this IDeclarativeProperty> from) + public static IDeclarativeProperty Switch(this IDeclarativeProperty?> from) => new SwitchProperty(from); } \ No newline at end of file diff --git a/src/Library/DeclarativeProperty/SwitchProperty.cs b/src/Library/DeclarativeProperty/SwitchProperty.cs index 51bdb7c..da861d8 100644 --- a/src/Library/DeclarativeProperty/SwitchProperty.cs +++ b/src/Library/DeclarativeProperty/SwitchProperty.cs @@ -4,13 +4,13 @@ public sealed class SwitchProperty : DeclarativePropertyBase { private IDisposable? _innerSubscription; - public SwitchProperty(IDeclarativeProperty?> from) : base(from.Value is null ? default : from.Value.Value) + public SwitchProperty(IDeclarativeProperty?> from) : base(from.Value is null ? default : from.Value.Value) { AddDisposable(from.Subscribe(HandleStreamChange)); _innerSubscription = from.Value?.Subscribe(HandleInnerValueChange); } - private async Task HandleStreamChange(IDeclarativeProperty? next, CancellationToken token) + private async Task HandleStreamChange(IDeclarativeProperty? next, CancellationToken token) { _innerSubscription?.Dispose(); _innerSubscription = next?.Subscribe(HandleInnerValueChange); diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs new file mode 100644 index 0000000..b50bd5f --- /dev/null +++ b/src/Library/TerminalUI/Binding.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.Reflection; +using TerminalUI.Controls; +using TerminalUI.Traits; + +namespace TerminalUI; + +public class Binding : IDisposable +{ + private readonly Func _dataContextMapper; + private IView _view; + private object? _propertySource; + private PropertyInfo _targetProperty; + + public Binding( + IView view, + Func 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.DataContext)) return; + + _targetProperty.SetValue(_propertySource, _dataContextMapper(_view.DataContext)); + } + + public void Dispose() + { + _view.PropertyChanged -= View_PropertyChanged; + _view = null!; + _propertySource = null!; + _targetProperty = null!; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs new file mode 100644 index 0000000..67e9a6c --- /dev/null +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using TerminalUI.Traits; + +namespace TerminalUI.Controls; + +public interface IView : INotifyPropertyChanged, IDisposableCollection +{ + T? DataContext { get; set; } + void Render(); + + TChild CreateChild() + where TChild : IView, new(); + + TChild CreateChild(Func dataContextMapper) + where TChild : IView, new(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs new file mode 100644 index 0000000..71b5d28 --- /dev/null +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -0,0 +1,85 @@ +using System.Buffers; +using System.Collections.ObjectModel; +using DeclarativeProperty; + +namespace TerminalUI.Controls; + +public class ListView : View +{ + private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared; + + private readonly List _itemsDisposables = new(); + private Func>? _getItems; + private object? _itemsSource; + private ListViewItem[]? _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> observableDeclarativeProperty) + _getItems = () => observableDeclarativeProperty.Value; + else if (_itemsSource is IDeclarativeProperty> readOnlyObservableDeclarativeProperty) + _getItems = () => readOnlyObservableDeclarativeProperty.Value; + else if (_itemsSource is IDeclarativeProperty> enumerableDeclarativeProperty) + _getItems = () => enumerableDeclarativeProperty.Value; + else if (_itemsSource is ICollection collection) + _getItems = () => collection; + else if (_itemsSource is TItem[] array) + _getItems = () => array; + else if (_itemsSource is IEnumerable 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> 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, TItem>(_ => dataContext); + } + + _listViewItems = newListViewItems; + _listViewItemLength = items.Count; + listViewItems = newListViewItems[..items.Count]; + } + else + { + listViewItems = _listViewItems[.._listViewItemLength]; + } + + foreach (var item in listViewItems) + { + item.Render(); + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs new file mode 100644 index 0000000..307a2a7 --- /dev/null +++ b/src/Library/TerminalUI/Controls/ListViewItem.cs @@ -0,0 +1,9 @@ +namespace TerminalUI.Controls; + +public class ListViewItem : View +{ + public override void Render() + { + Console.WriteLine(DataContext?.ToString()); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs new file mode 100644 index 0000000..ab93bb7 --- /dev/null +++ b/src/Library/TerminalUI/Controls/View.cs @@ -0,0 +1,73 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace TerminalUI.Controls; + +public abstract class View : IView +{ + private readonly ConcurrentBag _disposables = new(); + public T? DataContext { get; set; } + public abstract void Render(); + + public TChild CreateChild() where TChild : IView, new() + { + var child = new TChild + { + DataContext = DataContext + }; + var mapper = new DataContextMapper(this, d => child.DataContext = d); + AddDisposable(mapper); + child.AddDisposable(mapper); + + return child; + } + + public TChild CreateChild(Func dataContextMapper) + where TChild : IView, new() + { + var child = new TChild + { + DataContext = dataContextMapper(DataContext) + }; + var mapper = new DataContextMapper(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(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 + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + + _disposables.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/DataContextMapper.cs b/src/Library/TerminalUI/DataContextMapper.cs new file mode 100644 index 0000000..8031a56 --- /dev/null +++ b/src/Library/TerminalUI/DataContextMapper.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using TerminalUI.Controls; + +namespace TerminalUI; + +public class DataContextMapper : IDisposable +{ + private readonly IView _source; + private readonly Action _setter; + + public DataContextMapper(IView source, Action setter) + { + ArgumentNullException.ThrowIfNull(source); + + _source = source; + _setter = setter; + source.PropertyChanged += SourceOnPropertyChanged; + } + + private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(IView.DataContext)) return; + _setter(_source.DataContext); + } + + public void Dispose() => _source.PropertyChanged -= SourceOnPropertyChanged; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/Binding.cs new file mode 100644 index 0000000..dd2d7f9 --- /dev/null +++ b/src/Library/TerminalUI/Extensions/Binding.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; +using System.Reflection; +using TerminalUI.Controls; + +namespace TerminalUI.Extensions; + +public static class Binding +{ + public static Binding Bind( + this TView targetView, + IView view, + Expression> dataContextExpression, + Expression> 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(view, dataContextMapper, targetView, propertyInfo); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/TerminalUI.csproj b/src/Library/TerminalUI/TerminalUI.csproj new file mode 100644 index 0000000..778f1f3 --- /dev/null +++ b/src/Library/TerminalUI/TerminalUI.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/Library/TerminalUI/Traits/IDisposableCollection.cs b/src/Library/TerminalUI/Traits/IDisposableCollection.cs new file mode 100644 index 0000000..df8d761 --- /dev/null +++ b/src/Library/TerminalUI/Traits/IDisposableCollection.cs @@ -0,0 +1,6 @@ +namespace TerminalUI.Traits; + +public interface IDisposableCollection : IDisposable +{ + void AddDisposable(IDisposable disposable); +} \ No newline at end of file