Custom TUI Library WIP
This commit is contained in:
@@ -11,8 +11,4 @@
|
|||||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Terminal.Gui" Version="1.13.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
using FileTime.App.Core.Services;
|
using FileTime.App.Core.Services;
|
||||||
using FileTime.ConsoleUI.App.Extensions;
|
|
||||||
using FileTime.ConsoleUI.App.KeyInputHandling;
|
using FileTime.ConsoleUI.App.KeyInputHandling;
|
||||||
using FileTime.Core.Models;
|
|
||||||
using Terminal.Gui;
|
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -10,7 +7,7 @@ public class App : IApplication
|
|||||||
{
|
{
|
||||||
private readonly ILifecycleService _lifecycleService;
|
private readonly ILifecycleService _lifecycleService;
|
||||||
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 IKeyInputHandlerService _keyInputHandlerService;
|
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
||||||
|
|
||||||
@@ -18,13 +15,13 @@ public class App : IApplication
|
|||||||
ILifecycleService lifecycleService,
|
ILifecycleService lifecycleService,
|
||||||
IKeyInputHandlerService keyInputHandlerService,
|
IKeyInputHandlerService keyInputHandlerService,
|
||||||
IConsoleAppState consoleAppState,
|
IConsoleAppState consoleAppState,
|
||||||
IAppKeyService<Key> appKeyService,
|
//IAppKeyService<Key> appKeyService,
|
||||||
MainWindow mainWindow)
|
MainWindow mainWindow)
|
||||||
{
|
{
|
||||||
_lifecycleService = lifecycleService;
|
_lifecycleService = lifecycleService;
|
||||||
_keyInputHandlerService = keyInputHandlerService;
|
_keyInputHandlerService = keyInputHandlerService;
|
||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
_appKeyService = appKeyService;
|
//_appKeyService = appKeyService;
|
||||||
_mainWindow = mainWindow;
|
_mainWindow = mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,24 +31,5 @@ public class App : IApplication
|
|||||||
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait();
|
||||||
|
|
||||||
_mainWindow.Initialize();
|
_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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<ObservableCollection<IItemViewModel>?> _source;
|
|
||||||
|
|
||||||
public ItemRenderer(
|
|
||||||
IDeclarativeProperty<ObservableCollection<IItemViewModel>?> 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;
|
|
||||||
}
|
|
||||||
@@ -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<Key> appKeyService)
|
|
||||||
{
|
|
||||||
var maybeKey = appKeyService.MapKey(args.Key);
|
|
||||||
if (maybeKey is not { } key1) return null;
|
|
||||||
return new GeneralKeyEventArgs
|
|
||||||
{
|
|
||||||
Key = key1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,16 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||||
<PackageReference Include="Terminal.Gui" Version="1.13.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core\FileTime.App.Core.csproj" />
|
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core\FileTime.App.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
|
||||||
<ProjectReference Include="..\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj" />
|
<ProjectReference Include="..\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Controls\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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<Key>
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<Key, Keys> KeyMapping;
|
|
||||||
|
|
||||||
//TODO: write test for this. Test if every enum value is present in the dictionary.
|
|
||||||
public static ReadOnlyDictionary<Key, Keys> KeyMappingReadOnly { get; }
|
|
||||||
|
|
||||||
static ConsoleAppKeyService()
|
|
||||||
{
|
|
||||||
KeyMapping = new Dictionary<Key, Keys>
|
|
||||||
{
|
|
||||||
{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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
using DeclarativeProperty;
|
using System.Collections.ObjectModel;
|
||||||
using FileTime.ConsoleUI.App.Controls;
|
using DeclarativeProperty;
|
||||||
|
using FileTime.App.Core.ViewModels;
|
||||||
using FileTime.ConsoleUI.App.Extensions;
|
using FileTime.ConsoleUI.App.Extensions;
|
||||||
using Terminal.Gui;
|
using TerminalUI;
|
||||||
|
using TerminalUI.Controls;
|
||||||
|
using TerminalUI.Extensions;
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
public class MainWindow
|
public class MainWindow
|
||||||
{
|
{
|
||||||
private readonly IConsoleAppState _consoleAppState;
|
private readonly IConsoleAppState _consoleAppState;
|
||||||
private View[] _views;
|
|
||||||
private const int ParentColumnWidth = 20;
|
private const int ParentColumnWidth = 20;
|
||||||
|
|
||||||
public MainWindow(IConsoleAppState consoleAppState)
|
public MainWindow(IConsoleAppState consoleAppState)
|
||||||
@@ -16,67 +18,16 @@ public class MainWindow
|
|||||||
_consoleAppState = consoleAppState;
|
_consoleAppState = consoleAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize() =>
|
public void Initialize()
|
||||||
_views = new View[]
|
|
||||||
{
|
|
||||||
GetSelectedItemsView(),
|
|
||||||
GetParentsChildren(),
|
|
||||||
GetSelectedsChildren()
|
|
||||||
};
|
|
||||||
|
|
||||||
private ListView GetSelectedItemsView()
|
|
||||||
{
|
{
|
||||||
ListView selectedItemsView = new() {X = ParentColumnWidth, Y = 1, Width = Dim.Percent(60) - 20, Height = Dim.Fill()};
|
ListView<IAppState, IItemViewModel> selectedItemsView = new();
|
||||||
var selectedsItems = _consoleAppState
|
selectedItemsView.DataContext = _consoleAppState;
|
||||||
.SelectedTab
|
|
||||||
.Map(t => t.CurrentItems)
|
|
||||||
.Switch();
|
|
||||||
|
|
||||||
var selectedItem = _consoleAppState.SelectedTab
|
selectedItemsView.Bind(
|
||||||
.Map(t => t.CurrentSelectedItem)
|
selectedItemsView,
|
||||||
.Switch();
|
appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
|
||||||
|
v => v.ItemsSource);
|
||||||
DeclarativePropertyHelpers.CombineLatest(
|
|
||||||
selectedItem,
|
selectedItemsView.Render();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<View> GetElements() => _views;
|
|
||||||
}
|
}
|
||||||
@@ -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<IEnumerable<FullName>> GetFilesAsync() => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public Task SetFilesAsync(IEnumerable<FullName> files) => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ 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 Terminal.Gui;
|
|
||||||
|
|
||||||
namespace FileTime.ConsoleUI.App;
|
namespace FileTime.ConsoleUI.App;
|
||||||
|
|
||||||
@@ -19,8 +18,6 @@ public static class Startup
|
|||||||
services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>());
|
services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>());
|
||||||
services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>();
|
services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>();
|
||||||
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
|
||||||
services.TryAddSingleton<IAppKeyService<Key>, ConsoleAppKeyService>();
|
|
||||||
services.TryAddSingleton<ISystemClipboardService, SystemClipboardService>();
|
|
||||||
services.AddSingleton<CustomLoggerSink>();
|
services.AddSingleton<CustomLoggerSink>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.ContainerSizeS
|
|||||||
EndProject
|
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}"
|
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
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\TerminalUI\TerminalUI.csproj", "{2F01FC4C-D942-48B0-B61C-7C5BEAED4787}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{81F44BBB-6F89-41B4-89F1-4A3204843DB5}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -369,6 +375,7 @@ Global
|
|||||||
{E5FD38ED-6E4B-42AA-850B-470B939B836B} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
{E5FD38ED-6E4B-42AA-850B-470B939B836B} = {A5291117-3001-498B-AC8B-E14F71F72570}
|
||||||
{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}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}
|
||||||
|
|||||||
@@ -91,6 +91,6 @@ public static class DeclarativePropertyExtensions
|
|||||||
Action<TResult?>? setValueHook = null)
|
Action<TResult?>? setValueHook = null)
|
||||||
=> new CombineLatestProperty<T1,T2,TResult?>(prop1, prop2, func, setValueHook);
|
=> 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);
|
=> new SwitchProperty<T>(from);
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,13 @@ public sealed class SwitchProperty<TItem> : DeclarativePropertyBase<TItem>
|
|||||||
{
|
{
|
||||||
private IDisposable? _innerSubscription;
|
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));
|
AddDisposable(from.Subscribe(HandleStreamChange));
|
||||||
_innerSubscription = from.Value?.Subscribe(HandleInnerValueChange);
|
_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?.Dispose();
|
||||||
_innerSubscription = next?.Subscribe(HandleInnerValueChange);
|
_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