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