From 2528487ff6aa24b52615fc3b441af0ea4edf6e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 8 Aug 2023 18:28:13 +0200 Subject: [PATCH] Terminal UI V2, advanced binding --- ...FileTime.ConsoleUI.App.Abstractions.csproj | 1 + .../ITheme.cs | 20 +++ src/ConsoleApp/FileTime.ConsoleUI.App/App.cs | 48 +++++- .../KeyInputHandling/ConsoleAppKeyService.cs | 91 ++++++++++ .../FileTime.ConsoleUI.App/MainWindow.cs | 60 +++++-- .../Services/ConsoleSystemClipboardService.cs | 13 ++ .../FileTime.ConsoleUI.App/Startup.cs | 10 +- .../FileTime.ConsoleUI.Styles/DefaultTheme.cs | 53 ++++++ .../FileTime.ConsoleUI.Styles.csproj | 13 ++ src/ConsoleApp/FileTime.ConsoleUI/DI.cs | 4 +- .../FileTime.ConsoleUI.csproj | 2 + src/ConsoleApp/FileTime.ConsoleUI/Program.cs | 48 +++++- src/FileTime.sln | 7 + .../DeclarativePropertyBase.cs | 27 ++- src/Library/TerminalUI/ApplicationContext.cs | 5 +- src/Library/TerminalUI/Binding.cs | 155 +++++++++++++----- .../TerminalUI/ConsoleDrivers/DotnetDriver.cs | 41 +++++ .../ConsoleDrivers/IConsoleDriver.cs | 19 +++ .../ConsoleDrivers/WindowsDriver.cs | 16 ++ .../TerminalUI/Controls/ContentView.cs | 7 +- src/Library/TerminalUI/Controls/IView.cs | 9 +- src/Library/TerminalUI/Controls/ListView.cs | 23 ++- .../TerminalUI/Controls/ListViewItem.cs | 6 +- src/Library/TerminalUI/Controls/TextBlock.cs | 38 ++++- src/Library/TerminalUI/Controls/View.cs | 16 +- src/Library/TerminalUI/EventLoop.cs | 115 +++---------- src/Library/TerminalUI/Extensions/Binding.cs | 6 +- src/Library/TerminalUI/IApplicationContext.cs | 5 +- src/Library/TerminalUI/IEventLoop.cs | 1 + src/Library/TerminalUI/Models/Color256.cs | 14 ++ src/Library/TerminalUI/Models/ColorRGB.cs | 14 ++ src/Library/TerminalUI/Models/ColorType.cs | 8 + src/Library/TerminalUI/Models/Colors.cs | 83 ++++++++++ src/Library/TerminalUI/Models/ConsoleColor.cs | 6 + src/Library/TerminalUI/Models/IColor.cs | 7 + src/Library/TerminalUI/Models/Position.cs | 3 + .../TerminalUI/PropertyChangeTracker.cs | 113 +++++++++++++ .../TerminalUI/Traits/IContentRenderer.cs | 3 +- 38 files changed, 911 insertions(+), 199 deletions(-) create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App/Services/ConsoleSystemClipboardService.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.Styles/FileTime.ConsoleUI.Styles.csproj create mode 100644 src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs create mode 100644 src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs create mode 100644 src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs create mode 100644 src/Library/TerminalUI/Models/Color256.cs create mode 100644 src/Library/TerminalUI/Models/ColorRGB.cs create mode 100644 src/Library/TerminalUI/Models/ColorType.cs create mode 100644 src/Library/TerminalUI/Models/Colors.cs create mode 100644 src/Library/TerminalUI/Models/ConsoleColor.cs create mode 100644 src/Library/TerminalUI/Models/IColor.cs create mode 100644 src/Library/TerminalUI/Models/Position.cs create mode 100644 src/Library/TerminalUI/PropertyChangeTracker.cs 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 22168c5..9108811 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 @@ -9,6 +9,7 @@ + diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs new file mode 100644 index 0000000..ade5fed --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs @@ -0,0 +1,20 @@ +using TerminalUI.Models; + +namespace FileTime.ConsoleUI.App; + +public interface ITheme +{ + IColor? ItemBackgroundColor { get; } + IColor? AlternativeItemBackgroundColor { get; } + IColor? SelectedItemBackgroundColor { get; } + IColor? MarkedItemBackgroundColor { get; } + IColor? MarkedAlternativeItemBackgroundColor { get; } + IColor? MarkedSelectedItemBackgroundColor { get; } + IColor? DefaultForegroundColor { get; } + IColor? DefaultBackgroundColor { get; } + IColor? AlternativeItemForegroundColor { get; } + IColor? SelectedItemForegroundColor { get; } + IColor? MarkedItemForegroundColor { get; } + IColor? MarkedAlternativeItemForegroundColor { get; } + IColor? MarkedSelectedItemForegroundColor { get; } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index a7b1d8a..2f9e8d6 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -1,32 +1,42 @@ -using FileTime.App.Core.Services; +using FileTime.App.Core.Models; +using FileTime.App.Core.Services; using FileTime.ConsoleUI.App.KeyInputHandling; using TerminalUI; +using TerminalUI.ConsoleDrivers; namespace FileTime.ConsoleUI.App; 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 IApplicationContext _applicationContext; + private readonly IConsoleDriver _consoleDriver; private readonly IKeyInputHandlerService _keyInputHandlerService; + private readonly Thread _renderThread; public App( ILifecycleService lifecycleService, IKeyInputHandlerService keyInputHandlerService, IConsoleAppState consoleAppState, - //IAppKeyService appKeyService, + IAppKeyService appKeyService, MainWindow mainWindow, - IApplicationContext applicationContext) + IApplicationContext applicationContext, + IConsoleDriver consoleDriver) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; _consoleAppState = consoleAppState; - //_appKeyService = appKeyService; + _appKeyService = appKeyService; _mainWindow = mainWindow; _applicationContext = applicationContext; + _consoleDriver = consoleDriver; + + _renderThread = new Thread(Render); } public void Run() @@ -34,7 +44,31 @@ public class App : IApplication Task.Run(async () => await _lifecycleService.InitStartupHandlersAsync()).Wait(); _mainWindow.Initialize(); - - _applicationContext.EventLoop.Run(); + foreach (var rootView in _mainWindow.RootViews()) + { + _applicationContext.EventLoop.AddViewToRender(rootView); + } + + _applicationContext.IsRunning = true; + _renderThread.Start(); + + while (_applicationContext.IsRunning) + { + if (_consoleDriver.CanRead()) + { + var key = _consoleDriver.ReadKey(); + if (_appKeyService.MapKey(key.Key) is { } mappedKey) + { + var keyEventArgs = new GeneralKeyEventArgs + { + Key = mappedKey + }; + _keyInputHandlerService.HandleKeyInput(keyEventArgs); + } + } + Thread.Sleep(10); + } } + + private void Render() => _applicationContext.EventLoop.Run(); } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs new file mode 100644 index 0000000..e29343e --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs @@ -0,0 +1,91 @@ +using System.Collections.ObjectModel; +using FileTime.App.Core.Models; +using FileTime.App.Core.Services; + +namespace FileTime.ConsoleUI.App.KeyInputHandling; + +public 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 + { + {ConsoleKey.A, Keys.A}, + {ConsoleKey.B, Keys.B}, + {ConsoleKey.C, Keys.C}, + {ConsoleKey.D, Keys.D}, + {ConsoleKey.E, Keys.E}, + {ConsoleKey.F, Keys.F}, + {ConsoleKey.G, Keys.G}, + {ConsoleKey.H, Keys.H}, + {ConsoleKey.I, Keys.I}, + {ConsoleKey.J, Keys.J}, + {ConsoleKey.K, Keys.K}, + {ConsoleKey.L, Keys.L}, + {ConsoleKey.M, Keys.M}, + {ConsoleKey.N, Keys.N}, + {ConsoleKey.O, Keys.O}, + {ConsoleKey.P, Keys.P}, + {ConsoleKey.Q, Keys.Q}, + {ConsoleKey.R, Keys.R}, + {ConsoleKey.S, Keys.S}, + {ConsoleKey.T, Keys.T}, + {ConsoleKey.U, Keys.U}, + {ConsoleKey.V, Keys.V}, + {ConsoleKey.W, Keys.W}, + {ConsoleKey.X, Keys.X}, + {ConsoleKey.Y, Keys.Y}, + {ConsoleKey.Z, Keys.Z}, + {ConsoleKey.F1, Keys.F1}, + {ConsoleKey.F2, Keys.F2}, + {ConsoleKey.F3, Keys.F3}, + {ConsoleKey.F4, Keys.F4}, + {ConsoleKey.F5, Keys.F5}, + {ConsoleKey.F6, Keys.F6}, + {ConsoleKey.F7, Keys.F7}, + {ConsoleKey.F8, Keys.F8}, + {ConsoleKey.F9, Keys.F9}, + {ConsoleKey.F10, Keys.F10}, + {ConsoleKey.F11, Keys.F11}, + {ConsoleKey.F12, Keys.F12}, + {ConsoleKey.D0, Keys.Num0}, + {ConsoleKey.D1, Keys.Num1}, + {ConsoleKey.D2, Keys.Num2}, + {ConsoleKey.D3, Keys.Num3}, + {ConsoleKey.D4, Keys.Num4}, + {ConsoleKey.D5, Keys.Num5}, + {ConsoleKey.D6, Keys.Num6}, + {ConsoleKey.D7, Keys.Num7}, + {ConsoleKey.D8, Keys.Num8}, + {ConsoleKey.D9, Keys.Num9}, + {ConsoleKey.UpArrow, Keys.Up}, + {ConsoleKey.DownArrow, Keys.Down}, + {ConsoleKey.LeftArrow, Keys.Left}, + {ConsoleKey.RightArrow, Keys.Right}, + {ConsoleKey.Enter, Keys.Enter}, + {ConsoleKey.Escape, Keys.Escape}, + {ConsoleKey.Backspace, Keys.Backspace}, + {ConsoleKey.Spacebar, Keys.Space}, + {ConsoleKey.PageUp, Keys.PageUp}, + {ConsoleKey.PageDown, Keys.PageDown}, + {ConsoleKey.OemComma, Keys.Comma}, + {(ConsoleKey)0xA1, Keys.Question}, + {ConsoleKey.Tab, Keys.Tab}, + {ConsoleKey.LeftWindows, Keys.LWin}, + {ConsoleKey.RightWindows, Keys.RWin}, + }; + + KeyMappingReadOnly = new(KeyMapping); + } + + public Keys? MapKey(ConsoleKey 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 d822397..704491f 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,9 +1,12 @@ -using System.Linq.Expressions; +using System.Collections.ObjectModel; +using System.Linq.Expressions; using DeclarativeProperty; +using FileTime.App.Core.Models.Enums; using FileTime.App.Core.ViewModels; using TerminalUI; using TerminalUI.Controls; using TerminalUI.Extensions; +using TerminalUI.Models; namespace FileTime.ConsoleUI.App; @@ -11,22 +14,28 @@ public class MainWindow { private readonly IConsoleAppState _consoleAppState; private readonly IApplicationContext _applicationContext; - private const int ParentColumnWidth = 20; + private readonly ITheme _theme; + private ListView _selectedItemsView; - public MainWindow(IConsoleAppState consoleAppState, IApplicationContext applicationContext) + public MainWindow( + IConsoleAppState consoleAppState, + IApplicationContext applicationContext, + ITheme theme) { _consoleAppState = consoleAppState; _applicationContext = applicationContext; + _theme = theme; } public void Initialize() { - ListView selectedItemsView = new() + _selectedItemsView = new() { + DataContext = _consoleAppState, ApplicationContext = _applicationContext }; - selectedItemsView.DataContext = _consoleAppState; - selectedItemsView.ItemTemplate = item => + + _selectedItemsView.ItemTemplate = item => { var textBlock = item.CreateChild>(); textBlock.Bind( @@ -34,15 +43,44 @@ public class MainWindow dc => dc == null ? string.Empty : dc.DisplayNameText, tb => tb.Text ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value), + tb => tb.Foreground + ); return textBlock; }; - selectedItemsView.Bind( - selectedItemsView, - appState => appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), + _selectedItemsView.Bind( + _selectedItemsView, + appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), v => v.ItemsSource); - - selectedItemsView.RequestRerender(); } + + public IEnumerable RootViews() => new IView[] {_selectedItemsView}; + + private IColor? ToForegroundColor(ItemViewMode viewMode) + => viewMode switch + { + ItemViewMode.Default => _theme.DefaultForegroundColor, + ItemViewMode.Alternative => _theme.AlternativeItemForegroundColor, + ItemViewMode.Selected => _theme.SelectedItemForegroundColor, + ItemViewMode.Marked => _theme.MarkedItemForegroundColor, + ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemForegroundColor, + ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemForegroundColor, + _ => throw new NotImplementedException() + }; + + private IColor? ToBackgroundColor(ItemViewMode viewMode) + => viewMode switch + { + ItemViewMode.Default => _theme.DefaultBackgroundColor, + ItemViewMode.Alternative => _theme.AlternativeItemBackgroundColor, + ItemViewMode.Selected => _theme.SelectedItemBackgroundColor, + ItemViewMode.Marked => _theme.MarkedItemBackgroundColor, + ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemBackgroundColor, + ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemBackgroundColor, + _ => throw new NotImplementedException() + }; } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/ConsoleSystemClipboardService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/ConsoleSystemClipboardService.cs new file mode 100644 index 0000000..362bdb2 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/ConsoleSystemClipboardService.cs @@ -0,0 +1,13 @@ +using FileTime.App.Core.Services; +using FileTime.Core.Models; + +namespace FileTime.ConsoleUI.App.Services; + +public class ConsoleSystemClipboardService : ISystemClipboardService +{ + public Task CopyToClipboardAsync(string text) => throw new NotImplementedException(); + + 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 4e1c353..bc34e16 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -6,6 +6,7 @@ using FileTime.Core.Interactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using TerminalUI; +using TerminalUI.ConsoleDrivers; namespace FileTime.ConsoleUI.App; @@ -19,9 +20,16 @@ public static class Startup services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton, ConsoleAppKeyService>(); + services.TryAddSingleton(); services.AddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(sp + => new ApplicationContext + { + ConsoleDriver = sp.GetRequiredService() + } + ); return services; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs new file mode 100644 index 0000000..8055b86 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -0,0 +1,53 @@ +using FileTime.ConsoleUI.App; +using TerminalUI.Models; +using ConsoleColor = TerminalUI.Models.ConsoleColor; + +namespace FileTime.ConsoleUI.Styles; + +public record Theme( + IColor? ItemBackgroundColor, + IColor? AlternativeItemBackgroundColor, + IColor? SelectedItemBackgroundColor, + IColor? MarkedItemBackgroundColor, + IColor? MarkedAlternativeItemBackgroundColor, + IColor? MarkedSelectedItemBackgroundColor, + IColor? DefaultForegroundColor, + IColor? DefaultBackgroundColor, + IColor? AlternativeItemForegroundColor, + IColor? SelectedItemForegroundColor, + IColor? MarkedItemForegroundColor, + IColor? MarkedAlternativeItemForegroundColor, + IColor? MarkedSelectedItemForegroundColor) : ITheme; + +public static class DefaultThemes +{ + public static Theme Color256Theme => new( + ItemBackgroundColor: Color256Colors.Backgrounds.Black, + AlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black, + SelectedItemBackgroundColor: Color256Colors.Backgrounds.Black, + MarkedItemBackgroundColor: Color256Colors.Backgrounds.Black, + MarkedAlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black, + MarkedSelectedItemBackgroundColor: Color256Colors.Backgrounds.Black, + DefaultForegroundColor: null, + DefaultBackgroundColor: null, + AlternativeItemForegroundColor: null, + SelectedItemForegroundColor: Color256Colors.Foregrounds.Black, + MarkedItemForegroundColor: Color256Colors.Foregrounds.White, + MarkedAlternativeItemForegroundColor: Color256Colors.Foregrounds.White, + MarkedSelectedItemForegroundColor: Color256Colors.Foregrounds.Cyan); + + public static Theme ConsoleColorTheme => new( + ItemBackgroundColor: ConsoleColors.Foregrounds.Black, + AlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black, + SelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black, + MarkedItemBackgroundColor: ConsoleColors.Foregrounds.Black, + MarkedAlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black, + MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black, + DefaultForegroundColor: null, + DefaultBackgroundColor: null, + AlternativeItemForegroundColor: null, + SelectedItemForegroundColor: ConsoleColors.Foregrounds.Black, + MarkedItemForegroundColor: ConsoleColors.Foregrounds.White, + MarkedAlternativeItemForegroundColor: ConsoleColors.Foregrounds.White, + MarkedSelectedItemForegroundColor: ConsoleColors.Foregrounds.Cyan); +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/FileTime.ConsoleUI.Styles.csproj b/src/ConsoleApp/FileTime.ConsoleUI.Styles/FileTime.ConsoleUI.Styles.csproj new file mode 100644 index 0000000..079946b --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/FileTime.ConsoleUI.Styles.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs index 20b9ac5..8aa2fb9 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs @@ -18,9 +18,9 @@ public static class DI { public static IServiceProvider ServiceProvider { get; private set; } = null!; - public static void Initialize(IConfigurationRoot configuration) + public static void Initialize(IConfigurationRoot configuration, IServiceCollection serviceCollection) => ServiceProvider = DependencyInjection - .RegisterDefaultServices(configuration: configuration) + .RegisterDefaultServices(configuration: configuration, serviceCollection: serviceCollection) .AddConsoleServices() .AddLocalProviderServices() .AddServerCoreServices() diff --git a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj index dfd6801..55412d3 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI/FileTime.ConsoleUI.csproj @@ -17,6 +17,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs index b59fbe8..739b025 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs @@ -2,20 +2,52 @@ using FileTime.App.Core.Configuration; using FileTime.ConsoleUI; using FileTime.ConsoleUI.App; +using FileTime.ConsoleUI.Styles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using TerminalUI.ConsoleDrivers; -(AppDataRoot, EnvironmentName) = Init.InitDevelopment(); -var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(MainConfiguration.Configuration) +IConsoleDriver driver = new WindowsDriver(); +driver.Init(); +ITheme theme; +if (driver.GetCursorPosition() is not {PosX: 0, PosY: 0}) +{ + driver = new DotnetDriver(); + driver.Init(); + theme = DefaultThemes.ConsoleColorTheme; +} +else +{ + theme = DefaultThemes.Color256Theme; +} + +driver.SetCursorVisible(false); + +try +{ + (AppDataRoot, EnvironmentName) = Init.InitDevelopment(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(MainConfiguration.Configuration) #if DEBUG - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) #endif - .Build(); -DI.Initialize(configuration); + .Build(); -var app = DI.ServiceProvider.GetRequiredService(); -app.Run(); + var serviceCollection = new ServiceCollection(); + serviceCollection.TryAddSingleton(driver); + serviceCollection.TryAddSingleton(theme); + + DI.Initialize(configuration, serviceCollection); + + var app = DI.ServiceProvider.GetRequiredService(); + app.Run(); +} +finally +{ + driver.SetCursorVisible(true); + driver.Dispose(); +} public partial class Program { diff --git a/src/FileTime.sln b/src/FileTime.sln index 561d26e..d2099c5 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -121,6 +121,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI", "Library\Termi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\CircularBuffer\CircularBuffer.csproj", "{AF4FE804-12D9-46E2-A584-BFF6D4509766}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", "ConsoleApp\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj", "{CCB6F86A-7E80-448E-B543-DF9DB337C42A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -327,6 +329,10 @@ Global {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 + {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -383,6 +389,7 @@ Global {81F44BBB-6F89-41B4-89F1-4A3204843DB5} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs b/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs index e00e2b9..55fb65b 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyBase.cs @@ -12,6 +12,7 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty private readonly List, T>> _unsubscribeTriggers = new(); private readonly List _triggerDisposables = new(); private readonly object _triggerLock = new(); + private readonly object _subscriberLock = new(); private T? _value; @@ -35,7 +36,12 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty protected async Task NotifySubscribersAsync(T? newValue, CancellationToken cancellationToken = default) { - var subscribers = _subscribers.ToList(); + List> subscribers; + lock (_subscriberLock) + { + subscribers = _subscribers.ToList(); + } + foreach (var handler in subscribers) { await handler(newValue, cancellationToken); @@ -50,13 +56,23 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty public IDisposable Subscribe(Func onChange) { - _subscribers.Add(onChange); + lock (_subscriberLock) + { + _subscribers.Add(onChange); + } + onChange(_value, default); return new Unsubscriber(this, onChange); } - public void Unsubscribe(Func onChange) => _subscribers.Remove(onChange); + public void Unsubscribe(Func onChange) + { + lock (_subscriberLock) + { + _subscribers.Remove(onChange); + } + } public IDeclarativeProperty RegisterTrigger( Func, T, IDisposable?> triggerSubscribe, @@ -139,7 +155,10 @@ public abstract class DeclarativePropertyBase : IDeclarativeProperty disposable.Dispose(); } - _subscribers.Clear(); + lock (_subscriberLock) + { + _subscribers.Clear(); + } } protected void AddDisposable(IDisposable disposable) => _disposables.Add(disposable); diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index 7bdb0ba..b936485 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -1,7 +1,10 @@ -namespace TerminalUI; +using TerminalUI.ConsoleDrivers; + +namespace TerminalUI; public class ApplicationContext : IApplicationContext { + public required IConsoleDriver ConsoleDriver { get; init; } public IEventLoop EventLoop { get; init; } public bool IsRunning { get; set; } diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 38b2a73..2d961ba 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -12,16 +12,15 @@ public class Binding : IDisposable private IView _dataSourceView; private object? _propertySource; private PropertyInfo _targetProperty; - private readonly List _rerenderProperties; - private readonly IDisposableCollection? _propertySourceDisposableCollection; - private INotifyPropertyChanged? _dataSourceLastDataContext; + private IDisposableCollection? _propertySourceDisposableCollection; + private PropertyTrackTreeItem? _propertyTrackTreeItem; + private IPropertyChangeTracker? _propertyChangeTracker; public Binding( IView dataSourceView, Expression> dataContextExpression, object? propertySource, - PropertyInfo targetProperty, - IEnumerable? rerenderProperties = null + PropertyInfo targetProperty ) { ArgumentNullException.ThrowIfNull(dataSourceView); @@ -31,70 +30,148 @@ public class Binding : IDisposable _dataContextMapper = dataContextExpression.Compile(); _propertySource = propertySource; _targetProperty = targetProperty; - _rerenderProperties = rerenderProperties?.ToList() ?? new List(); - FindReactiveProperties(dataContextExpression); + InitTrackingTree(dataContextExpression); + + UpdateTrackers(); dataSourceView.PropertyChanged += View_PropertyChanged; - var initialValue = _dataContextMapper(_dataSourceView.DataContext); - _targetProperty.SetValue(_propertySource, initialValue); + UpdateTargetProperty(); + AddToSourceDisposables(propertySource); + + dataSourceView.AddDisposable(this); + } + + private void AddToSourceDisposables(object? propertySource) + { 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) + private void InitTrackingTree(Expression> dataContextExpression) + { + var properties = new List(); + FindReactiveProperties(dataContextExpression, properties); + + if (properties.Count > 0) + { + var rootItem = new PropertyTrackTreeItem(); + foreach (var property in properties) + { + var pathParts = property.Split('.'); + var currentItem = rootItem; + for (var i = 0; i < pathParts.Length; i++) + { + if (!currentItem.Children.TryGetValue(pathParts[i], out var child)) + { + child = new PropertyTrackTreeItem(); + currentItem.Children.Add(pathParts[i], child); + } + + currentItem = child; + } + } + + _propertyTrackTreeItem = rootItem; + } + } + + private string? FindReactiveProperties(Expression expression, List properties) { if (expression is LambdaExpression lambdaExpression) { - FindReactiveProperties(lambdaExpression.Body); + SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties)); } else if (expression is ConditionalExpression conditionalExpression) { - FindReactiveProperties(conditionalExpression.IfFalse); - FindReactiveProperties(conditionalExpression.IfTrue); + SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties)); + SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties)); } - else if (expression is MemberExpression {Member: PropertyInfo dataContextPropertyInfo}) + else if (expression is MemberExpression memberExpression) { - _rerenderProperties.Add(dataContextPropertyInfo.Name); + if (memberExpression.Expression is not null) + { + FindReactiveProperties(memberExpression.Expression, properties); + + if (FindReactiveProperties(memberExpression.Expression, properties) is { } path + && memberExpression.Member is PropertyInfo dataContextPropertyInfo) + { + path += "." + memberExpression.Member.Name; + return path; + } + } + } + else if (expression is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Object is + { + NodeType: + not ExpressionType.Parameter + and not ExpressionType.Constant + } methodObject) + { + SavePropertyPath(FindReactiveProperties(methodObject, properties)); + } + + foreach (var argument in methodCallExpression.Arguments) + { + SavePropertyPath(FindReactiveProperties(argument, properties)); + } + } + else if (expression is BinaryExpression binaryExpression) + { + SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties)); + SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties)); + } + else if (expression is UnaryExpression unaryExpression) + { + SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties)); + } + else if (expression is ParameterExpression parameterExpression) + { + if (parameterExpression.Type == typeof(TDataContext)) + { + return ""; + } + } + + return null; + + void SavePropertyPath(string? path) + { + if (path is null) return; + path = path.TrimStart('.'); + properties.Add(path); } - //TODO: Handle other expression types } private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IView.DataContext)) return; - if (_dataSourceLastDataContext is not null) - { - _dataSourceLastDataContext.PropertyChanged -= DataContext_PropertyChanged; - } - - if (_dataSourceView.DataContext is INotifyPropertyChanged dataSourcePropertyChanged) - { - _dataSourceLastDataContext = dataSourcePropertyChanged; - dataSourcePropertyChanged.PropertyChanged += DataContext_PropertyChanged; - } - + UpdateTrackers(); UpdateTargetProperty(); } - private void DataContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) + private void UpdateTrackers() { - if (e.PropertyName == null - || !_rerenderProperties.Contains(e.PropertyName)) return; - UpdateTargetProperty(); + if (_propertyChangeTracker is not null) + { + _propertyChangeTracker.Dispose(); + } + + if (_propertyTrackTreeItem is not null) + { + _propertyChangeTracker = PropertyChangeHelper.TraverseDataContext( + _propertyTrackTreeItem, + _dataSourceView.DataContext, + UpdateTargetProperty + ); + } } private void UpdateTargetProperty() diff --git a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs new file mode 100644 index 0000000..8a28efa --- /dev/null +++ b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs @@ -0,0 +1,41 @@ +using TerminalUI.Models; +using ConsoleColor = TerminalUI.Models.ConsoleColor; + +namespace TerminalUI.ConsoleDrivers; + +public class DotnetDriver : IConsoleDriver +{ + public virtual void Init() => Console.Clear(); + + public void SetCursorPosition(Position position) => Console.SetCursorPosition(position.PosX, position.PosY); + + public void ResetColor() => Console.ResetColor(); + + public Position GetCursorPosition() + { + var (x, y) = Console.GetCursorPosition(); + return new(x, y); + } + + public void Write(string text) => Console.Write(text); + + public void Write(char text) => Console.Write(text); + + public virtual void Dispose() {} + + public bool CanRead() => Console.KeyAvailable; + public ConsoleKeyInfo ReadKey() => Console.ReadKey(true); + + public void SetCursorVisible(bool cursorVisible) => Console.CursorVisible = cursorVisible; + public virtual void SetForegroundColor(IColor foreground) + { + if (foreground is not ConsoleColor consoleColor) throw new NotSupportedException(); + Console.ForegroundColor = consoleColor.Color; + } + + public virtual void SetBackgroundColor(IColor background) + { + if (background is not ConsoleColor consoleColor) throw new NotSupportedException(); + Console.ForegroundColor = consoleColor.Color; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs new file mode 100644 index 0000000..52b2632 --- /dev/null +++ b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs @@ -0,0 +1,19 @@ +using TerminalUI.Models; + +namespace TerminalUI.ConsoleDrivers; + +public interface IConsoleDriver +{ + void Init(); + void Dispose(); + void SetCursorPosition(Position position); + void ResetColor(); + Position GetCursorPosition(); + void Write(string text); + void Write(char text); + bool CanRead(); + ConsoleKeyInfo ReadKey(); + void SetCursorVisible(bool cursorVisible); + void SetForegroundColor(IColor foreground); + void SetBackgroundColor(IColor background); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs new file mode 100644 index 0000000..927e431 --- /dev/null +++ b/src/Library/TerminalUI/ConsoleDrivers/WindowsDriver.cs @@ -0,0 +1,16 @@ +using TerminalUI.Models; + +namespace TerminalUI.ConsoleDrivers; + +public sealed class WindowsDriver : DotnetDriver +{ + public override void Init() => Console.Out.Write("\x1b[?1049h"); + + public override void Dispose() => Console.Out.Write("\x1b[?1049l"); + + public override void SetBackgroundColor(IColor background) + => Write(background.ToConsoleColor()); + + public override void SetForegroundColor(IColor foreground) + => Write(foreground.ToConsoleColor()); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ContentView.cs b/src/Library/TerminalUI/Controls/ContentView.cs index 0a2ba0b..1e0955e 100644 --- a/src/Library/TerminalUI/Controls/ContentView.cs +++ b/src/Library/TerminalUI/Controls/ContentView.cs @@ -1,4 +1,5 @@ -using TerminalUI.Traits; +using TerminalUI.Models; +using TerminalUI.Traits; namespace TerminalUI.Controls; @@ -9,7 +10,7 @@ public abstract class ContentView: View, IContentRenderer ContentRendererMethod = DefaultContentRender; } public IView? Content { get; set; } - public Action ContentRendererMethod { get; set; } + public Action ContentRendererMethod { get; set; } - private void DefaultContentRender() => Content?.Render(); + private void DefaultContentRender(Position position) => Content?.Render(position); } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index beb36e1..8305d54 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using TerminalUI.Models; using TerminalUI.Traits; namespace TerminalUI.Controls; @@ -6,12 +7,10 @@ namespace TerminalUI.Controls; public interface IView : INotifyPropertyChanged, IDisposableCollection { object? DataContext { get; set; } - Action RenderMethod { get; set; } - IApplicationContext ApplicationContext { get; init;} + Action RenderMethod { get; set; } + IApplicationContext? ApplicationContext { get; init;} event Action Disposed; - event Action RenderRequested; - void Render(); - void RequestRerender(); + void Render(Position position); } public interface IView : IView diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index 4b4cbc5..a6c680c 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections.ObjectModel; using DeclarativeProperty; +using TerminalUI.Models; namespace TerminalUI.Controls; @@ -9,7 +10,7 @@ public class ListView : View private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared; private readonly List _itemsDisposables = new(); - private Func>? _getItems; + private Func?>? _getItems; private object? _itemsSource; private ListViewItem[]? _listViewItems; private int _listViewItemLength; @@ -30,11 +31,20 @@ public class ListView : View _itemsDisposables.Clear(); if (_itemsSource is IDeclarativeProperty> observableDeclarativeProperty) + { + observableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); _getItems = () => observableDeclarativeProperty.Value; + } else if (_itemsSource is IDeclarativeProperty> readOnlyObservableDeclarativeProperty) + { + readOnlyObservableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); _getItems = () => readOnlyObservableDeclarativeProperty.Value; + } else if (_itemsSource is IDeclarativeProperty> enumerableDeclarativeProperty) + { + enumerableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); _getItems = () => enumerableDeclarativeProperty.Value; + } else if (_itemsSource is ICollection collection) _getItems = () => collection; else if (_itemsSource is TItem[] array) @@ -54,18 +64,20 @@ public class ListView : View public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; - protected override void DefaultRenderer() + protected override void DefaultRenderer(Position position) { var listViewItems = InstantiateItemViews(); + var deltaY = 0; foreach (var item in listViewItems) { - item.Render(); + item.Render(position with {PosY = position.PosY + deltaY++}); } } private Span> InstantiateItemViews() { - if (_getItems is null) + var items = _getItems?.Invoke()?.ToList(); + if (items is null) { if (_listViewItemLength != 0) { @@ -74,11 +86,10 @@ public class ListView : View return _listViewItems; } - var items = _getItems().ToList(); Span> listViewItems; - if (_listViewItems is null || _listViewItems.Length != items.Count) + if (_listViewItems is null || _listViewItemLength != items.Count) { var newListViewItems = ListViewItemPool.Rent(items.Count); for (var i = 0; i < items.Count; i++) diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs index 55c4092..e9ecdfc 100644 --- a/src/Library/TerminalUI/Controls/ListViewItem.cs +++ b/src/Library/TerminalUI/Controls/ListViewItem.cs @@ -1,10 +1,10 @@ -using TerminalUI.Traits; +using TerminalUI.Models; namespace TerminalUI.Controls; public class ListViewItem : ContentView { - protected override void DefaultRenderer() + protected override void DefaultRenderer(Position position) { if (ContentRendererMethod is null) { @@ -16,6 +16,6 @@ public class ListViewItem : ContentView + DataContext?.GetType().Name); } - ContentRendererMethod(); + ContentRendererMethod(position); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index 4df745c..a23a65c 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -1,11 +1,18 @@ using PropertyChanged.SourceGenerator; using TerminalUI.Extensions; +using TerminalUI.Models; namespace TerminalUI.Controls; public partial class TextBlock : View { + private record RenderContext(Position Position, string? Text, IColor? Foreground, IColor? Background); + + private RenderContext? _renderContext; + [Notify] private string? _text = string.Empty; + [Notify] private IColor? _foreground; + [Notify] private IColor? _background; public TextBlock() { @@ -16,8 +23,35 @@ public partial class TextBlock : View ); RerenderProperties.Add(nameof(Text)); + RerenderProperties.Add(nameof(Foreground)); + RerenderProperties.Add(nameof(Background)); } - protected override void DefaultRenderer() - => Console.Write(Text); + protected override void DefaultRenderer(Position position) + { + var driver = ApplicationContext!.ConsoleDriver; + var renderContext = new RenderContext(position, Text, _foreground, _background); + if (!NeedsRerender(renderContext)) return; + + _renderContext = renderContext; + + if (Text is null) return; + + driver.SetCursorPosition(position); + driver.ResetColor(); + if (Foreground is { } foreground) + { + driver.SetForegroundColor(foreground); + } + + if (Background is { } background) + { + driver.SetBackgroundColor(background); + } + + driver.Write(Text); + } + + private bool NeedsRerender(RenderContext renderContext) + => _renderContext is null || _renderContext != renderContext; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index d8397f8..e9fb209 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using PropertyChanged.SourceGenerator; +using TerminalUI.Models; namespace TerminalUI.Controls; @@ -9,10 +10,9 @@ public abstract partial class View : IView { private readonly List _disposables = new(); [Notify] private T? _dataContext; - public Action RenderMethod { get; set; } - public IApplicationContext ApplicationContext { get; init; } + public Action RenderMethod { get; set; } + public IApplicationContext? ApplicationContext { get; init; } public event Action? Disposed; - public event Action? RenderRequested; protected List RerenderProperties { get; } = new(); protected View() @@ -29,13 +29,13 @@ public abstract partial class View : IView ) ) { - RenderRequested?.Invoke(this); + ApplicationContext?.EventLoop.RequestRerender(); } } - protected abstract void DefaultRenderer(); + protected abstract void DefaultRenderer(Position position); - public void Render() + public void Render(Position position) { if (RenderMethod is null) { @@ -47,11 +47,9 @@ public abstract partial class View : IView + DataContext?.GetType().Name); } - RenderMethod(); + RenderMethod(position); } - public void RequestRerender() => RenderRequested?.Invoke(this); - public TChild CreateChild() where TChild : IView, new() { var child = new TChild diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index 6199d5b..6f0dfed 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -1,6 +1,5 @@ -using System.Buffers; -using System.Collections.Concurrent; -using TerminalUI.Controls; +using TerminalUI.Controls; +using TerminalUI.Models; namespace TerminalUI; @@ -8,10 +7,8 @@ public class EventLoop : IEventLoop { private readonly IApplicationContext _applicationContext; private readonly object _lock = new(); - private readonly ArrayPool _viewPool = ArrayPool.Shared; - - private readonly ConcurrentBag _viewsToRenderInstantly = new(); - private readonly LinkedList _viewsToRender = new(); + private readonly List _viewsToRender = new(); + private bool _rerenderRequested; public EventLoop(IApplicationContext applicationContext) { @@ -21,6 +18,7 @@ public class EventLoop : IEventLoop public void Run() { _applicationContext.IsRunning = true; + _rerenderRequested = true; while (_applicationContext.IsRunning) { Render(); @@ -28,103 +26,36 @@ public class EventLoop : IEventLoop } } + public void RequestRerender() + { + lock (_lock) + { + _rerenderRequested = true; + } + } + public void Render() { - IView[]? viewsToRenderCopy = null; - IView[]? viewsAlreadyRendered = null; - try + List viewsToRender; + lock (_lock) { - int viewsToRenderCopyCount; - IView[]? viewsToRenderInstantly; + if (!_rerenderRequested) return; + _rerenderRequested = false; - 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; + viewsToRender = _viewsToRender.ToList(); } - return false; + foreach (var view in viewsToRender) + { + view.Render(new Position(0, 0)); + } } public void AddViewToRender(IView view) { lock (_lock) { - _viewsToRender.AddLast(view); + _viewsToRender.Add(view); } } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/Binding.cs index ad5e74b..09c9662 100644 --- a/src/Library/TerminalUI/Extensions/Binding.cs +++ b/src/Library/TerminalUI/Extensions/Binding.cs @@ -10,8 +10,7 @@ public static class Binding this TView targetView, IView dataSourceView, Expression> dataContextExpression, - Expression> propertyExpression, - IEnumerable? rerenderProperties = null) + Expression> propertyExpression) { if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo}) throw new AggregateException(nameof(propertyExpression) + " must be a property expression"); @@ -20,8 +19,7 @@ public static class Binding dataSourceView, dataContextExpression, targetView, - propertyInfo, - rerenderProperties + propertyInfo ); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index fcc23f3..9a26400 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -1,7 +1,10 @@ -namespace TerminalUI; +using TerminalUI.ConsoleDrivers; + +namespace TerminalUI; public interface IApplicationContext { IEventLoop EventLoop { get; init; } bool IsRunning { get; set; } + IConsoleDriver ConsoleDriver { get; init; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IEventLoop.cs b/src/Library/TerminalUI/IEventLoop.cs index 91a39d5..fc6dba5 100644 --- a/src/Library/TerminalUI/IEventLoop.cs +++ b/src/Library/TerminalUI/IEventLoop.cs @@ -7,4 +7,5 @@ public interface IEventLoop void Render(); void AddViewToRender(IView view); void Run(); + void RequestRerender(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Color256.cs b/src/Library/TerminalUI/Models/Color256.cs new file mode 100644 index 0000000..c1a4dc9 --- /dev/null +++ b/src/Library/TerminalUI/Models/Color256.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace TerminalUI.Models; + +public record struct Color256(byte Color, ColorType Type) : IColor +{ + public string ToConsoleColor() + => Type switch + { + ColorType.Foreground => $"\x1b[38;5;{Color}m", + ColorType.Background => $"\x1b[48;5;{Color}m", + _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) + }; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/ColorRGB.cs b/src/Library/TerminalUI/Models/ColorRGB.cs new file mode 100644 index 0000000..cb9c028 --- /dev/null +++ b/src/Library/TerminalUI/Models/ColorRGB.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace TerminalUI.Models; + +public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor +{ + public string ToConsoleColor() + => Type switch + { + ColorType.Foreground => $"\x1b[38;2;{R};{G};{B};m", + ColorType.Background => $"\x1b[48;2;{R};{G};{B};m", + _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) + }; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/ColorType.cs b/src/Library/TerminalUI/Models/ColorType.cs new file mode 100644 index 0000000..3223daf --- /dev/null +++ b/src/Library/TerminalUI/Models/ColorType.cs @@ -0,0 +1,8 @@ +namespace TerminalUI.Models; + +public enum ColorType +{ + Unknown, + Foreground, + Background +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Colors.cs b/src/Library/TerminalUI/Models/Colors.cs new file mode 100644 index 0000000..ec53eae --- /dev/null +++ b/src/Library/TerminalUI/Models/Colors.cs @@ -0,0 +1,83 @@ +namespace TerminalUI.Models; + +public static class Color256Colors +{ + public static class Backgrounds + { + public static readonly Color256 Black = new(0, ColorType.Background); + public static readonly Color256 Blue = new(9, ColorType.Background); + public static readonly Color256 Cyan = new(11, ColorType.Background); + public static readonly Color256 DarkBlue = new(1, ColorType.Background); + public static readonly Color256 DarkCyan = new(3, ColorType.Background); + public static readonly Color256 DarkGray = new(8, ColorType.Background); + public static readonly Color256 DarkGreen = new(2, ColorType.Background); + public static readonly Color256 DarkMagenta = new(5, ColorType.Background); + public static readonly Color256 DarkRed = new(4, ColorType.Background); + public static readonly Color256 DarkYellow = new(6, ColorType.Background); + public static readonly Color256 Gray = new(7, ColorType.Background); + public static readonly Color256 Green = new(10, ColorType.Background); + public static readonly Color256 Magenta = new(13, ColorType.Background); + public static readonly Color256 Red = new(12, ColorType.Background); + public static readonly Color256 White = new(15, ColorType.Background); + } + public static class Foregrounds + { + public static readonly Color256 Black = new(0, ColorType.Foreground); + public static readonly Color256 Blue = new(9, ColorType.Foreground); + public static readonly Color256 Cyan = new(11, ColorType.Foreground); + public static readonly Color256 DarkBlue = new(1, ColorType.Foreground); + public static readonly Color256 DarkCyan = new(3, ColorType.Foreground); + public static readonly Color256 DarkGray = new(8, ColorType.Foreground); + public static readonly Color256 DarkGreen = new(2, ColorType.Foreground); + public static readonly Color256 DarkMagenta = new(5, ColorType.Foreground); + public static readonly Color256 DarkRed = new(4, ColorType.Foreground); + public static readonly Color256 DarkYellow = new(6, ColorType.Foreground); + public static readonly Color256 Gray = new(7, ColorType.Foreground); + public static readonly Color256 Green = new(10, ColorType.Foreground); + public static readonly Color256 Magenta = new(13, ColorType.Foreground); + public static readonly Color256 Red = new(12, ColorType.Foreground); + public static readonly Color256 White = new(15, ColorType.Foreground); + } +} + +public static class ConsoleColors +{ + public static class Backgrounds + { + public static readonly ConsoleColor Black = new(System.ConsoleColor.Black, ColorType.Background); + public static readonly ConsoleColor Blue = new(System.ConsoleColor.Blue, ColorType.Background); + public static readonly ConsoleColor Cyan = new(System.ConsoleColor.Cyan, ColorType.Background); + public static readonly ConsoleColor DarkBlue = new(System.ConsoleColor.DarkBlue, ColorType.Background); + public static readonly ConsoleColor DarkCyan = new(System.ConsoleColor.DarkCyan, ColorType.Background); + public static readonly ConsoleColor DarkGray = new(System.ConsoleColor.DarkGray, ColorType.Background); + public static readonly ConsoleColor DarkGreen = new(System.ConsoleColor.DarkGreen, ColorType.Background); + public static readonly ConsoleColor DarkMagenta = new(System.ConsoleColor.DarkMagenta, ColorType.Background); + public static readonly ConsoleColor DarkRed = new(System.ConsoleColor.DarkRed, ColorType.Background); + public static readonly ConsoleColor DarkYellow = new(System.ConsoleColor.DarkYellow, ColorType.Background); + public static readonly ConsoleColor Gray = new(System.ConsoleColor.Gray, ColorType.Background); + public static readonly ConsoleColor Green = new(System.ConsoleColor.Green, ColorType.Background); + public static readonly ConsoleColor Magenta = new(System.ConsoleColor.Magenta, ColorType.Background); + public static readonly ConsoleColor Red = new(System.ConsoleColor.Red, ColorType.Background); + public static readonly ConsoleColor White = new(System.ConsoleColor.White, ColorType.Background); + public static readonly ConsoleColor Yellow = new(System.ConsoleColor.Yellow, ColorType.Background); + } + public static class Foregrounds + { + public static readonly ConsoleColor Black = new(System.ConsoleColor.Black, ColorType.Foreground); + public static readonly ConsoleColor Blue = new(System.ConsoleColor.Blue, ColorType.Foreground); + public static readonly ConsoleColor Cyan = new(System.ConsoleColor.Cyan, ColorType.Foreground); + public static readonly ConsoleColor DarkBlue = new(System.ConsoleColor.DarkBlue, ColorType.Foreground); + public static readonly ConsoleColor DarkCyan = new(System.ConsoleColor.DarkCyan, ColorType.Foreground); + public static readonly ConsoleColor DarkGray = new(System.ConsoleColor.DarkGray, ColorType.Foreground); + public static readonly ConsoleColor DarkGreen = new(System.ConsoleColor.DarkGreen, ColorType.Foreground); + public static readonly ConsoleColor DarkMagenta = new(System.ConsoleColor.DarkMagenta, ColorType.Foreground); + public static readonly ConsoleColor DarkRed = new(System.ConsoleColor.DarkRed, ColorType.Foreground); + public static readonly ConsoleColor DarkYellow = new(System.ConsoleColor.DarkYellow, ColorType.Foreground); + public static readonly ConsoleColor Gray = new(System.ConsoleColor.Gray, ColorType.Foreground); + public static readonly ConsoleColor Green = new(System.ConsoleColor.Green, ColorType.Foreground); + public static readonly ConsoleColor Magenta = new(System.ConsoleColor.Magenta, ColorType.Foreground); + public static readonly ConsoleColor Red = new(System.ConsoleColor.Red, ColorType.Foreground); + public static readonly ConsoleColor White = new(System.ConsoleColor.White, ColorType.Foreground); + public static readonly ConsoleColor Yellow = new(System.ConsoleColor.Yellow, ColorType.Foreground); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/ConsoleColor.cs b/src/Library/TerminalUI/Models/ConsoleColor.cs new file mode 100644 index 0000000..1ebcd58 --- /dev/null +++ b/src/Library/TerminalUI/Models/ConsoleColor.cs @@ -0,0 +1,6 @@ +namespace TerminalUI.Models; + +public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor +{ + public string ToConsoleColor() => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/IColor.cs b/src/Library/TerminalUI/Models/IColor.cs new file mode 100644 index 0000000..cb0a683 --- /dev/null +++ b/src/Library/TerminalUI/Models/IColor.cs @@ -0,0 +1,7 @@ +namespace TerminalUI.Models; + +public interface IColor +{ + ColorType Type { get; } + string ToConsoleColor(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Position.cs b/src/Library/TerminalUI/Models/Position.cs new file mode 100644 index 0000000..2c40367 --- /dev/null +++ b/src/Library/TerminalUI/Models/Position.cs @@ -0,0 +1,3 @@ +namespace TerminalUI.Models; + +public record struct Position(int PosX, int PosY); \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyChangeTracker.cs b/src/Library/TerminalUI/PropertyChangeTracker.cs new file mode 100644 index 0000000..3c24e49 --- /dev/null +++ b/src/Library/TerminalUI/PropertyChangeTracker.cs @@ -0,0 +1,113 @@ +using System.ComponentModel; + +namespace TerminalUI; + +internal interface IPropertyChangeTracker : IDisposable +{ + Dictionary Children { get; } +} + +internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker +{ + public Dictionary Children { get; } = new(); + + public virtual void Dispose() + { + foreach (var propertyChangeTracker in Children.Values) + { + propertyChangeTracker.Dispose(); + } + } +} + +internal class PropertyChangeTracker : PropertyChangeTrackerBase +{ + private readonly PropertyTrackTreeItem _propertyTrackTreeItem; + private readonly INotifyPropertyChanged _target; + private readonly IEnumerable _propertiesToListen; + private readonly Action _updateBinding; + + public PropertyChangeTracker( + PropertyTrackTreeItem propertyTrackTreeItem, + INotifyPropertyChanged target, + IEnumerable propertiesToListen, + Action updateBinding) + { + _propertyTrackTreeItem = propertyTrackTreeItem; + _target = target; + _propertiesToListen = propertiesToListen; + _updateBinding = updateBinding; + target.PropertyChanged += Target_PropertyChanged; + } + + private void Target_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + var propertyName = e.PropertyName; + if (propertyName is null || !_propertiesToListen.Contains(propertyName)) + { + return; + } + + _updateBinding(); + Children.Remove(propertyName); + + var newChild = PropertyChangeHelper.TraverseDataContext( + _propertyTrackTreeItem.Children[propertyName], + _target.GetType().GetProperty(propertyName)?.GetValue(_target), + _updateBinding + ); + + if (newChild is not null) + { + Children.Add(propertyName, newChild); + } + } + + public override void Dispose() + { + _target.PropertyChanged -= Target_PropertyChanged; + + base.Dispose(); + } +} + +internal class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase +{ +} + +internal class PropertyTrackTreeItem +{ + public Dictionary Children { get; } = new(); +} + +internal static class PropertyChangeHelper +{ + internal static IPropertyChangeTracker? TraverseDataContext( + PropertyTrackTreeItem propertyTrackTreeItem, + object? obj, + Action updateBinding + ) + { + if (obj is null) return null; + + IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged + ? new PropertyChangeTracker(propertyTrackTreeItem, notifyPropertyChanged, propertyTrackTreeItem.Children.Keys, updateBinding) + : new NonSubscriberPropertyChangeTracker(); + + foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children) + { + var childTracker = TraverseDataContext( + trackerTreeItem, + obj.GetType().GetProperty(propertyName)?.GetValue(obj), + updateBinding + ); + + if (childTracker is not null) + { + tracker.Children.Add(propertyName, childTracker); + } + } + + return tracker; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Traits/IContentRenderer.cs b/src/Library/TerminalUI/Traits/IContentRenderer.cs index 6c8106a..d5106f2 100644 --- a/src/Library/TerminalUI/Traits/IContentRenderer.cs +++ b/src/Library/TerminalUI/Traits/IContentRenderer.cs @@ -1,9 +1,10 @@ using TerminalUI.Controls; +using TerminalUI.Models; namespace TerminalUI.Traits; public interface IContentRenderer { IView? Content { get; set; } - Action ContentRendererMethod { get; set; } + Action ContentRendererMethod { get; set; } } \ No newline at end of file