From 7dcca6363b185f559eb571c398c9bd12b430a660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Wed, 9 Aug 2023 20:40:54 +0200 Subject: [PATCH] New controls, main view --- .../ViewModels/IAppState.cs | 3 +- .../RapidTravelModeKeyInputHandler.cs | 4 +- .../NavigationUserCommandHandlerService.cs | 6 +- .../ViewModels/AppStateBase.cs | 7 +- .../ViewModels/ContainerViewModel.cs | 2 - .../ViewModels/ElementViewModel.cs | 2 - .../ViewModels/FileViewModel.cs | 2 - .../ViewModels/ItemViewModel.cs | 4 +- .../ITheme.cs | 14 +- .../FileTime.ConsoleUI.App/MainWindow.cs | 204 +++++++++++++++--- .../FileTime.ConsoleUI.App/Startup.cs | 4 +- .../FileTime.ConsoleUI.Styles/DefaultTheme.cs | 53 ++--- src/Core/FileTime.Core.Services/Tab.cs | 6 +- src/Library/TerminalUI/ApplicationContext.cs | 4 +- src/Library/TerminalUI/Binding.cs | 32 ++- src/Library/TerminalUI/Color/Color256.cs | 4 + src/Library/TerminalUI/Color/ColorRGB.cs | 3 + src/Library/TerminalUI/Color/ConsoleColor.cs | 3 + src/Library/TerminalUI/Color/IColor.cs | 2 + .../TerminalUI/ConsoleDrivers/DotnetDriver.cs | 2 +- .../ConsoleDrivers/IConsoleDriver.cs | 2 +- .../TerminalUI/Controls/ChildContainerView.cs | 58 +++++ .../TerminalUI/Controls/ChildInitializer.cs | 24 +++ src/Library/TerminalUI/Controls/Grid.cs | 170 +++++++++------ .../Controls/GridChildInitializer.cs | 24 --- .../TerminalUI/Controls/IChildContainer.cs | 8 + src/Library/TerminalUI/Controls/IView.cs | 4 +- src/Library/TerminalUI/Controls/ListView.cs | 94 ++++++-- src/Library/TerminalUI/Controls/StackPanel.cs | 54 +++++ src/Library/TerminalUI/Controls/TextBlock.cs | 16 +- src/Library/TerminalUI/Controls/View.cs | 2 +- src/Library/TerminalUI/EventLoop.cs | 4 +- src/Library/TerminalUI/Extensions/Binding.cs | 38 +++- .../TerminalUI/Extensions/ViewExtensions.cs | 6 + src/Library/TerminalUI/IApplicationContext.cs | 4 +- src/Library/TerminalUI/Models/Option.cs | 13 ++ src/Library/TerminalUI/Models/Orientation.cs | 7 + src/Library/TerminalUI/Models/Size.cs | 2 +- .../TerminalUI/Models/TextAlignment.cs | 8 + src/Library/TerminalUI/TerminalUI.csproj | 1 + .../ViewExtensions/GridPositionExtension.cs | 2 +- 41 files changed, 668 insertions(+), 234 deletions(-) create mode 100644 src/Library/TerminalUI/Controls/ChildContainerView.cs create mode 100644 src/Library/TerminalUI/Controls/ChildInitializer.cs delete mode 100644 src/Library/TerminalUI/Controls/GridChildInitializer.cs create mode 100644 src/Library/TerminalUI/Controls/IChildContainer.cs create mode 100644 src/Library/TerminalUI/Controls/StackPanel.cs create mode 100644 src/Library/TerminalUI/Models/Option.cs create mode 100644 src/Library/TerminalUI/Models/Orientation.cs create mode 100644 src/Library/TerminalUI/Models/TextAlignment.cs diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs index 40df07a..a9a7595 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs @@ -12,7 +12,7 @@ public interface IAppState IDeclarativeProperty SelectedTab { get; } IObservable SearchText { get; } IDeclarativeProperty ViewMode { get; } - DeclarativeProperty RapidTravelText { get; } + IDeclarativeProperty RapidTravelText { get; } IDeclarativeProperty RapidTravelTextDebounced { get; } IDeclarativeProperty ContainerStatus { get; } List PreviousKeys { get; } @@ -24,4 +24,5 @@ public interface IAppState void SetSearchText(string? searchText); Task SwitchViewModeAsync(ViewMode newViewMode); Task SetSelectedTabAsync(ITabViewModel tabToSelect); + Task SetRapidTravelTextAsync(string? text); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs index 7c2e7d9..8c2d0ff 100644 --- a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs @@ -75,7 +75,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler if (_appState.RapidTravelText.Value!.Length > 0) { args.Handled = true; - await _appState.RapidTravelText.SetValue( + await _appState.SetRapidTravelTextAsync( _appState.RapidTravelText.Value![..^1] ); } @@ -83,7 +83,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler else if (keyString.Length == 1) { args.Handled = true; - await _appState.RapidTravelText.SetValue( + await _appState.SetRapidTravelTextAsync( _appState.RapidTravelText.Value + keyString.ToLower() ); } diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index 03cad9e..0dc9126 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -251,7 +251,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase if (_currentSelectedItem?.Value is not IContainerViewModel containerViewModel || containerViewModel.Container is null) return; - await _appState.RapidTravelText.SetValue(""); + await _appState.SetRapidTravelTextAsync(""); if (_selectedTab?.Tab is { } tab) { await tab.SetCurrentLocation(containerViewModel.Container); @@ -260,13 +260,13 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase private async Task GoUp() { - if (_currentLocation?.Value?.Parent is not AbsolutePath parentPath || + if (_currentLocation?.Value?.Parent is not { } parentPath || await parentPath.ResolveAsyncSafe() is not IContainer newContainer) { return; } - await _appState.RapidTravelText.SetValue(""); + await _appState.SetRapidTravelTextAsync(""); if (_selectedTab?.Tab is { } tab) { await tab.SetCurrentLocation(newContainer); diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs index 7581236..ecfd85d 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -18,6 +18,7 @@ public abstract partial class AppStateBase : IAppState private readonly DeclarativeProperty _selectedTab = new(); private readonly DeclarativeProperty _viewMode = new(Models.Enums.ViewMode.Default); private readonly ObservableCollection _tabs = new(); + private readonly DeclarativeProperty _rapidTravelText; public IDeclarativeProperty ViewMode { get; } @@ -25,7 +26,7 @@ public abstract partial class AppStateBase : IAppState public IObservable SearchText { get; } public IDeclarativeProperty SelectedTab { get; } - public DeclarativeProperty RapidTravelText { get; } + public IDeclarativeProperty RapidTravelText { get; } public IDeclarativeProperty RapidTravelTextDebounced { get; } public IDeclarativeProperty ContainerStatus { get; } @@ -35,7 +36,8 @@ public abstract partial class AppStateBase : IAppState protected AppStateBase() { - RapidTravelText = new(""); + _rapidTravelText = new (""); + RapidTravelText = _rapidTravelText.DistinctUntilChanged(); RapidTravelTextDebounced = RapidTravelText .Debounce(v => string.IsNullOrEmpty(v) @@ -83,6 +85,7 @@ public abstract partial class AppStateBase : IAppState public async Task SwitchViewModeAsync(ViewMode newViewMode) => await _viewMode.SetValue(newViewMode); public async Task SetSelectedTabAsync(ITabViewModel tabToSelect) => await _selectedTab.SetValue(tabToSelect); + public async Task SetRapidTravelTextAsync(string? text) => await _rapidTravelText.SetValue(text); private ITabViewModel? GetSelectedTab(IEnumerable tabs, ITabViewModel? expectedSelectedTab) { diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs index 4c8fdd1..ce00f89 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ContainerViewModel.cs @@ -1,11 +1,9 @@ using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; using FileTime.Core.Models; -using MvvmGen; namespace FileTime.App.Core.ViewModels; -[ViewModel(GenerateConstructor = false)] public partial class ContainerViewModel : ItemViewModel, IContainerViewModel { public IContainer? Container => BaseItem as IContainer; diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs index 73b6577..68c1868 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ElementViewModel.cs @@ -2,11 +2,9 @@ using DeclarativeProperty; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; using FileTime.Core.Models; -using MvvmGen; namespace FileTime.App.Core.ViewModels; -[ViewModel(GenerateConstructor = false)] public partial class ElementViewModel : ItemViewModel, IElementViewModel { public IElement? Element => BaseItem as Element; diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs index 1350b42..5b13e55 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/FileViewModel.cs @@ -3,11 +3,9 @@ using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; using FileTime.Core.Models; using FileTime.Core.Models.Extensions; -using MvvmGen; namespace FileTime.App.Core.ViewModels; -[ViewModel(GenerateConstructor = false)] public partial class FileViewModel : ElementViewModel, IFileViewModel { public FileViewModel(IItemNameConverterService itemNameConverterService, IAppState appState) : base(itemNameConverterService, appState) diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs index 7197ba4..e187d7c 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs @@ -74,7 +74,7 @@ public abstract partial class ItemViewModel : IItemViewModel ? parentTab.CurrentSelectedItem .Map(EqualsTo) .DistinctUntilChanged() - .Debounce(TimeSpan.FromMilliseconds(10)) + .Debounce(TimeSpan.FromMilliseconds(1)) : new DeclarativeProperty(IsInDeepestPath()); IsAlternative = sourceCollection @@ -86,7 +86,7 @@ public abstract partial class ItemViewModel : IItemViewModel ViewMode = DeclarativePropertyHelpers .CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode) .DistinctUntilChanged() - .Debounce(TimeSpan.FromMilliseconds(100)); + .Debounce(TimeSpan.FromMilliseconds(1)); Attributes = item.Attributes; CreatedAt = item.CreatedAt; ModifiedAt = item.ModifiedAt; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs index cdf8f48..c9633ba 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs @@ -5,17 +5,9 @@ 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; } + IColor? ElementColor { get; } + IColor? ContainerColor { get; } + IColor? MarkedItemColor { get; } } \ 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 339f664..832d559 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,13 +1,11 @@ -using System.Collections.ObjectModel; -using System.Linq.Expressions; -using DeclarativeProperty; +using DeclarativeProperty; using FileTime.App.Core.Models.Enums; using FileTime.App.Core.ViewModels; +using FileTime.Core.Enums; using TerminalUI; using TerminalUI.Color; using TerminalUI.Controls; using TerminalUI.Extensions; -using TerminalUI.Models; using TerminalUI.ViewExtensions; using ConsoleColor = TerminalUI.Color.ConsoleColor; @@ -18,9 +16,8 @@ public class MainWindow private readonly IConsoleAppState _consoleAppState; private readonly IApplicationContext _applicationContext; private readonly ITheme _theme; - private ListView _selectedItemsView; - private Grid _grid; + private IView _root; public MainWindow( IConsoleAppState consoleAppState, @@ -34,13 +31,55 @@ public class MainWindow public void Initialize() { - _selectedItemsView = new() + var root = new Grid { DataContext = _consoleAppState, - ApplicationContext = _applicationContext + ApplicationContext = _applicationContext, + RowDefinitionsObject = "Auto *", + ChildInitializer = + { + new TextBlock() + .Setup(t => + t.Bind( + t, + appState => appState.SelectedTab.Value.CurrentLocation.Value.FullName.Path, + tb => tb.Text, + value => value + ) + ), + new Grid + { + ColumnDefinitionsObject = "* 4* 4*", + ChildInitializer = + { + ParentsItemsView(), + SelectedItemsView(), + SelectedsItemsView(), + }, + Extensions = + { + new GridPositionExtension(0, 1) + } + } + } + }; + _root = root; + } + + private ListView SelectedItemsView() + { + var list = new ListView + { + DataContext = _consoleAppState, + ApplicationContext = _applicationContext, + ListPadding = 8, + Extensions = + { + new GridPositionExtension(1, 0) + } }; - _selectedItemsView.ItemTemplate = item => + list.ItemTemplate = item => { var textBlock = item.CreateChild>(); textBlock.Bind( @@ -50,19 +89,120 @@ public class MainWindow ); textBlock.Bind( textBlock, - dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value), + dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type), tb => tb.Foreground ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type), + tb => tb.Background + ); return textBlock; }; - _selectedItemsView.Bind( - _selectedItemsView, + list.Bind( + list, appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), v => v.ItemsSource); - TestGrid(); + list.Bind( + list, + appState => + appState == null + ? null + : appState.SelectedTab.Value == null + ? null + : appState.SelectedTab.Value.CurrentSelectedItem.Value, + v => v.SelectedItem); + + return list; + } + + private ListView SelectedsItemsView() + { + var list = new ListView + { + DataContext = _consoleAppState, + ApplicationContext = _applicationContext, + ListPadding = 8, + Extensions = + { + new GridPositionExtension(2, 0) + } + }; + + list.ItemTemplate = item => + { + var textBlock = item.CreateChild>(); + textBlock.Bind( + textBlock, + dc => dc == null ? string.Empty : dc.DisplayNameText, + tb => tb.Text + ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type), + tb => tb.Foreground + ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type), + tb => tb.Background + ); + + return textBlock; + }; + + list.Bind( + list, + appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.SelectedsChildren).Switch(), + v => v.ItemsSource); + + return list; + } + + private ListView ParentsItemsView() + { + var list = new ListView + { + DataContext = _consoleAppState, + ApplicationContext = _applicationContext, + ListPadding = 8, + Extensions = + { + new GridPositionExtension(0, 0) + } + }; + + list.ItemTemplate = item => + { + var textBlock = item.CreateChild>(); + textBlock.Bind( + textBlock, + dc => dc == null ? string.Empty : dc.DisplayNameText, + tb => tb.Text + ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type), + tb => tb.Foreground + ); + textBlock.Bind( + textBlock, + dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type), + tb => tb.Background + ); + + return textBlock; + }; + + list.Bind( + list, + appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.ParentsChildren).Switch(), + v => v.ItemsSource); + + return list; } private void TestGrid() @@ -117,35 +257,37 @@ public class MainWindow } }; - _grid = grid; + //_grid = grid; } public IEnumerable RootViews() => new IView[] { - _grid, _selectedItemsView + _root }; - private IColor? ToForegroundColor(ItemViewMode viewMode) - => viewMode switch + private IColor? ToForegroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) => + (viewMode, absolutePathType) 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, + (ItemViewMode.Default, AbsolutePathType.Container) => _theme.ContainerColor, + (ItemViewMode.Alternative, AbsolutePathType.Container) => _theme.ContainerColor, + (ItemViewMode.Default, _) => _theme.ElementColor, + (ItemViewMode.Alternative, _) => _theme.ElementColor, + (ItemViewMode.Selected, _) => ToBackgroundColor(ItemViewMode.Default, absolutePathType)?.AsForeground(), + (ItemViewMode.Marked, _) => _theme.MarkedItemColor, + (ItemViewMode.MarkedSelected, _) => ToBackgroundColor(ItemViewMode.Marked, absolutePathType)?.AsForeground(), + (ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemColor, _ => throw new NotImplementedException() }; - private IColor? ToBackgroundColor(ItemViewMode viewMode) - => viewMode switch + private IColor? ToBackgroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) + => (viewMode, absolutePathType) 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, + (ItemViewMode.Default, _) => _theme.DefaultBackgroundColor, + (ItemViewMode.Alternative, _) => _theme.DefaultBackgroundColor, + (ItemViewMode.Selected, _) => ToForegroundColor(ItemViewMode.Default, absolutePathType)?.AsBackground(), + (ItemViewMode.Marked, _) => _theme.MarkedItemColor, + (ItemViewMode.MarkedSelected, _) => ToForegroundColor(ItemViewMode.Marked, absolutePathType)?.AsBackground(), + (ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemColor, _ => 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 13d5573..4c55ff6 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -8,6 +8,7 @@ using FileTime.Core.Interactions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using TerminalUI; using TerminalUI.ConsoleDrivers; @@ -33,7 +34,8 @@ public static class Startup services.TryAddSingleton(sp => new ApplicationContext { - ConsoleDriver = sp.GetRequiredService() + ConsoleDriver = sp.GetRequiredService(), + LoggerFactory = sp.GetRequiredService() } ); return services; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs index 4eecf21..a7de4f1 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -1,54 +1,29 @@ using FileTime.ConsoleUI.App; using TerminalUI.Color; -using TerminalUI.Models; -using ConsoleColor = TerminalUI.Color.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; + IColor? ElementColor, + IColor? ContainerColor, + IColor? MarkedItemColor) : 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); + DefaultForegroundColor: Color256Colors.Foregrounds.Gray, + DefaultBackgroundColor: Color256Colors.Foregrounds.Black, + ElementColor: Color256Colors.Foregrounds.Gray, + ContainerColor: Color256Colors.Foregrounds.Blue, + MarkedItemColor: Color256Colors.Foregrounds.Black + ); 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); + DefaultForegroundColor: ConsoleColors.Foregrounds.Gray, + DefaultBackgroundColor: ConsoleColors.Foregrounds.Black, + ElementColor: ConsoleColors.Foregrounds.Gray, + ContainerColor: ConsoleColors.Foregrounds.Blue, + MarkedItemColor: ConsoleColors.Foregrounds.Black); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index ea44171..05e8f57 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -23,7 +23,6 @@ public class Tab : ITab private AbsolutePath? _currentSelectedItemCached; private PointInTime _currentPointInTime; private CancellationTokenSource? _setCurrentLocationCancellationTokenSource; - private CancellationTokenSource? _setCurrentItemCancellationTokenSource; public IDeclarativeProperty CurrentLocation { get; } public IDeclarativeProperty?> CurrentItems { get; } @@ -214,9 +213,8 @@ public class Tab : ITab public async Task SetSelectedItem(AbsolutePath newSelectedItem) { - _setCurrentItemCancellationTokenSource?.Cancel(); - _setCurrentItemCancellationTokenSource = new CancellationTokenSource(); - await _currentRequestItem.SetValue(newSelectedItem, _setCurrentItemCancellationTokenSource.Token); + if (_currentRequestItem.Value is {} v && v.Path == newSelectedItem.Path) return; + await _currentRequestItem.SetValue(newSelectedItem); } public void AddItemFilter(ItemFilter filter) => _itemFilters.Add(filter); diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index b936485..2f5d6ae 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -1,10 +1,12 @@ -using TerminalUI.ConsoleDrivers; +using Microsoft.Extensions.Logging; +using TerminalUI.ConsoleDrivers; namespace TerminalUI; public class ApplicationContext : IApplicationContext { public required IConsoleDriver ConsoleDriver { get; init; } + public ILoggerFactory? LoggerFactory { 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 2d961ba..a769db3 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -6,30 +6,38 @@ using TerminalUI.Traits; namespace TerminalUI; -public class Binding : IDisposable +public class Binding : IDisposable { - private readonly Func _dataContextMapper; + private readonly Func _dataContextMapper; private IView _dataSourceView; private object? _propertySource; private PropertyInfo _targetProperty; + private readonly Func _converter; + private readonly TResult? _fallbackValue; private IDisposableCollection? _propertySourceDisposableCollection; private PropertyTrackTreeItem? _propertyTrackTreeItem; private IPropertyChangeTracker? _propertyChangeTracker; public Binding( IView dataSourceView, - Expression> dataContextExpression, + Expression> dataContextExpression, object? propertySource, - PropertyInfo targetProperty + PropertyInfo targetProperty, + Func converter, + TResult? fallbackValue = default ) { ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataContextExpression); ArgumentNullException.ThrowIfNull(targetProperty); + ArgumentNullException.ThrowIfNull(converter); + _dataSourceView = dataSourceView; _dataContextMapper = dataContextExpression.Compile(); _propertySource = propertySource; _targetProperty = targetProperty; + _converter = converter; + _fallbackValue = fallbackValue; InitTrackingTree(dataContextExpression); @@ -52,7 +60,7 @@ public class Binding : IDisposable } } - private void InitTrackingTree(Expression> dataContextExpression) + private void InitTrackingTree(Expression> dataContextExpression) { var properties = new List(); FindReactiveProperties(dataContextExpression, properties); @@ -175,7 +183,19 @@ public class Binding : IDisposable } private void UpdateTargetProperty() - => _targetProperty.SetValue(_propertySource, _dataContextMapper(_dataSourceView.DataContext)); + { + TResult value; + try + { + value = _converter(_dataContextMapper(_dataSourceView.DataContext)); + } + catch + { + value = _fallbackValue; + } + + _targetProperty.SetValue(_propertySource, value); + } public void Dispose() { diff --git a/src/Library/TerminalUI/Color/Color256.cs b/src/Library/TerminalUI/Color/Color256.cs index d79f58d..65dadf7 100644 --- a/src/Library/TerminalUI/Color/Color256.cs +++ b/src/Library/TerminalUI/Color/Color256.cs @@ -11,4 +11,8 @@ public record struct Color256(byte Color, ColorType Type) : IColor ColorType.Background => $"\x1b[48;5;{Color}m", _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) }; + + public IColor AsForeground() => this with {Type = ColorType.Foreground}; + + public IColor AsBackground() => this with {Type = ColorType.Background}; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Color/ColorRGB.cs b/src/Library/TerminalUI/Color/ColorRGB.cs index df83dfa..3a49e35 100644 --- a/src/Library/TerminalUI/Color/ColorRGB.cs +++ b/src/Library/TerminalUI/Color/ColorRGB.cs @@ -11,4 +11,7 @@ public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor ColorType.Background => $"\x1b[48;2;{R};{G};{B};m", _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) }; + public IColor AsForeground() => this with {Type = ColorType.Foreground}; + + public IColor AsBackground() => this with {Type = ColorType.Background}; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Color/ConsoleColor.cs b/src/Library/TerminalUI/Color/ConsoleColor.cs index 3968689..d014a3b 100644 --- a/src/Library/TerminalUI/Color/ConsoleColor.cs +++ b/src/Library/TerminalUI/Color/ConsoleColor.cs @@ -3,4 +3,7 @@ public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor { public string ToConsoleColor() => throw new NotImplementedException(); + public IColor AsForeground() => this with {Type = ColorType.Foreground}; + + public IColor AsBackground() => this with {Type = ColorType.Background}; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Color/IColor.cs b/src/Library/TerminalUI/Color/IColor.cs index e693ef2..4eb1f71 100644 --- a/src/Library/TerminalUI/Color/IColor.cs +++ b/src/Library/TerminalUI/Color/IColor.cs @@ -4,4 +4,6 @@ public interface IColor { ColorType Type { get; } string ToConsoleColor(); + IColor AsForeground(); + IColor AsBackground(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs index 0bcffe0..acb140f 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs @@ -44,6 +44,6 @@ public class DotnetDriver : IConsoleDriver Console.BackgroundColor = consoleColor.Color; } - public Size GetBufferSize() => new(Console.BufferWidth, Console.BufferHeight); + public Size GetWindowSize() => new(Console.WindowWidth, Console.WindowHeight); public void Clear() => Console.Clear(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs index 01646fe..b5ec25a 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs @@ -17,6 +17,6 @@ public interface IConsoleDriver void SetCursorVisible(bool cursorVisible); void SetForegroundColor(IColor foreground); void SetBackgroundColor(IColor background); - Size GetBufferSize(); + Size GetWindowSize(); void Clear(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ChildContainerView.cs b/src/Library/TerminalUI/Controls/ChildContainerView.cs new file mode 100644 index 0000000..042bd8c --- /dev/null +++ b/src/Library/TerminalUI/Controls/ChildContainerView.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; + +namespace TerminalUI.Controls; + +public abstract class ChildContainerView : View, IChildContainer +{ + private readonly ObservableCollection _children = new(); + public ReadOnlyObservableCollection Children { get; } + public ChildInitializer ChildInitializer { get; } + + protected ChildContainerView() + { + ChildInitializer = new ChildInitializer(this); + Children = new ReadOnlyObservableCollection(_children); + _children.CollectionChanged += (o, args) => + { + if (Attached) + { + if (args.NewItems?.OfType() is { } newItems) + { + foreach (var newItem in newItems) + { + newItem.Attached = true; + } + } + + ApplicationContext?.EventLoop.RequestRerender(); + } + }; + + ((INotifyPropertyChanged)this).PropertyChanged += (o, args) => + { + if (args.PropertyName == nameof(ApplicationContext)) + { + foreach (var child in Children) + { + child.ApplicationContext = ApplicationContext; + } + } + }; + } + + public override TChild AddChild(TChild child) + { + child = base.AddChild(child); + _children.Add(child); + return child; + } + + public override TChild AddChild(TChild child, Func dataContextMapper) + where TDataContext : default + { + child = base.AddChild(child, dataContextMapper); + _children.Add(child); + return child; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ChildInitializer.cs b/src/Library/TerminalUI/Controls/ChildInitializer.cs new file mode 100644 index 0000000..090310b --- /dev/null +++ b/src/Library/TerminalUI/Controls/ChildInitializer.cs @@ -0,0 +1,24 @@ +using System.Collections; + +namespace TerminalUI.Controls; + +public record ChildWithDataContextMapper(IView Child, Func DataContextMapper); + +public class ChildInitializer : IEnumerable +{ + private readonly IChildContainer _childContainer; + + public ChildInitializer(IChildContainer childContainer) + { + _childContainer = childContainer; + } + + public void Add(IView item) => _childContainer.AddChild(item); + + public void Add(ChildWithDataContextMapper item) + => _childContainer.AddChild(item.Child, item.DataContextMapper); + + public IEnumerator GetEnumerator() => _childContainer.Children.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index 0dc3529..9de5840 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -5,14 +5,15 @@ using TerminalUI.ViewExtensions; namespace TerminalUI.Controls; -public class Grid : View +public class Grid : ChildContainerView { + private delegate void WithSizes(Span widths, Span heights); + + private delegate TResult WithSizes(Span widths, Span heights); + private const int ToBeCalculated = -1; - private readonly ObservableCollection _children = new(); - public ReadOnlyObservableCollection Children { get; } - public GridChildInitializer ChildInitializer { get; } - public ObservableCollection RowDefinitions { get; } = new(); - public ObservableCollection ColumnDefinitions { get; } = new(); + public ObservableCollection RowDefinitions { get; } = new() {RowDefinition.Star(1)}; + public ObservableCollection ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)}; public object? ColumnDefinitionsObject { @@ -62,35 +63,76 @@ public class Grid : View } } - public Grid() - { - ChildInitializer = new GridChildInitializer(this); - Children = new ReadOnlyObservableCollection(_children); - _children.CollectionChanged += (o, e) => + public override Size GetRequestedSize() + => WithCalculatedSize((columnWidths, rowHeights) => { - if (Attached) + var width = 0; + var height = 0; + + for (var i = 0; i < columnWidths.Length; i++) { - if (e.NewItems?.OfType() is { } newItems) - { - foreach (var newItem in newItems) - { - newItem.Attached = true; - } - } - - ApplicationContext?.EventLoop.RequestRerender(); + width += columnWidths[i]; } - }; - } - public override Size GetRequestedSize() => throw new NotImplementedException(); + for (var i = 0; i < rowHeights.Length; i++) + { + height += rowHeights[i]; + } + + return new Size(width, height); + }, new Option(new Size(0, 0), false)); protected override void DefaultRenderer(Position position, Size size) + => WithCalculatedSize((columnWidths, rowHeights) => + { + foreach (var child in Children) + { + var positionExtension = child.GetExtension(); + var x = positionExtension?.Column ?? 0; + var y = positionExtension?.Row ?? 0; + + var width = columnWidths[x]; + var height = rowHeights[y]; + + var left = 0; + var top = 0; + + for (var i = 0; i < x; i++) + { + left += columnWidths[i]; + } + + for (var i = 0; i < y; i++) + { + top += rowHeights[i]; + } + + child.Render(new Position(position.X + left, position.Y + top), new Size(width, height)); + } + }, new Option(size, true)); + + private void WithCalculatedSize(WithSizes actionWithSizes, Option size) { - //TODO: Optimize it, dont calculate all of these only if there is Auto value(s) + WithCalculatedSize(Helper, size); + + object? Helper(Span widths, Span heights) + { + actionWithSizes(widths, heights); + return null; + } + } + + private TResult WithCalculatedSize(WithSizes actionWithSizes, Option size) + { + //TODO: Optimize it, dont calculate all of these, only if there is Auto value(s) var columns = ColumnDefinitions.Count; - Span allWidth = stackalloc int[columns * RowDefinitions.Count]; - Span allHeight = stackalloc int[columns * RowDefinitions.Count]; + var rows = RowDefinitions.Count; + + if (columns < 1) columns = 1; + if (rows < 1) rows = 1; + + Span allWidth = stackalloc int[columns * rows]; + Span allHeight = stackalloc int[columns * rows]; foreach (var child in Children) { @@ -104,38 +146,47 @@ public class Grid : View } Span columnWidths = stackalloc int[columns]; - Span rowHeights = stackalloc int[RowDefinitions.Count]; + Span rowHeights = stackalloc int[rows]; + var usedWidth = 0; + var widthStars = 0; for (var i = 0; i < columnWidths.Length; i++) { if (ColumnDefinitions[i].Type == GridUnitType.Pixel) { columnWidths[i] = ColumnDefinitions[i].Value; } - else if (ColumnDefinitions[i].Type == GridUnitType.Star) + else if (size.IsSome && ColumnDefinitions[i].Type == GridUnitType.Star) { + widthStars += ColumnDefinitions[i].Value; columnWidths[i] = ToBeCalculated; } else { var max = 0; - for (var j = 0; j < RowDefinitions.Count; j++) + for (var j = 0; j < rows; j++) { max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns)); } columnWidths[i] = max; } + + if (columnWidths[i] != ToBeCalculated) + usedWidth += columnWidths[i]; } + var usedHeight = 0; + var heightStars = 0; for (var i = 0; i < rowHeights.Length; i++) { if (RowDefinitions[i].Type == GridUnitType.Pixel) { rowHeights[i] = RowDefinitions[i].Value; } - else if (RowDefinitions[i].Type == GridUnitType.Star) + else if (size.IsSome && RowDefinitions[i].Type == GridUnitType.Star) { + heightStars += RowDefinitions[i].Value; rowHeights[i] = ToBeCalculated; } else @@ -148,33 +199,39 @@ public class Grid : View rowHeights[i] = max; } + + if (rowHeights[i] != ToBeCalculated) + usedHeight += rowHeights[i]; } - foreach (var child in Children) + if (size.IsSome) { - var childSize = child.GetRequestedSize(); - var positionExtension = child.GetExtension(); - var x = positionExtension?.Column ?? 0; - var y = positionExtension?.Row ?? 0; + var widthLeft = size.Value.Width - usedWidth; + var heightLeft = size.Value.Height - usedHeight; - var width = columnWidths[x]; - var height = rowHeights[y]; + var widthPerStart = (int) Math.Floor((double) widthLeft / widthStars); + var heightPerStart = (int) Math.Floor((double) heightLeft / heightStars); - var left = 0; - var top = 0; - - for (var i = 0; i < x; i++) + for (var i = 0; i < columnWidths.Length; i++) { - left += columnWidths[i]; + var column = ColumnDefinitions[i]; + if (column.Type == GridUnitType.Star) + { + columnWidths[i] = widthPerStart * column.Value; + } } - for (var i = 0; i < y; i++) + for (var i = 0; i < rowHeights.Length; i++) { - top += rowHeights[i]; + var row = RowDefinitions[i]; + if (row.Type == GridUnitType.Star) + { + rowHeights[i] = heightPerStart * row.Value; + } } - - child.Render(new Position(left, top), new Size(width, height)); } + + return actionWithSizes(columnWidths, rowHeights); } public void SetRowDefinitions(string value) @@ -190,7 +247,7 @@ public class Grid : View } else if (v.EndsWith("*")) { - var starValue = int.Parse(v[0..^1]); + var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]); RowDefinitions.Add(RowDefinition.Star(starValue)); } else if (int.TryParse(v, out var pixelValue)) @@ -217,7 +274,7 @@ public class Grid : View } else if (v.EndsWith("*")) { - var starValue = int.Parse(v[0..^1]); + var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]); ColumnDefinitions.Add(ColumnDefinition.Star(starValue)); } else if (int.TryParse(v, out var pixelValue)) @@ -230,19 +287,4 @@ public class Grid : View } } } - - public override TChild AddChild(TChild child) - { - child = base.AddChild(child); - _children.Add(child); - return child; - } - - public override TChild AddChild(TChild child, Func dataContextMapper) - where TDataContext : default - { - child = base.AddChild(child, dataContextMapper); - _children.Add(child); - return child; - } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/GridChildInitializer.cs b/src/Library/TerminalUI/Controls/GridChildInitializer.cs deleted file mode 100644 index 7643db7..0000000 --- a/src/Library/TerminalUI/Controls/GridChildInitializer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections; - -namespace TerminalUI.Controls; - -public record ChildWithDataContextMapper(IView Child, Func DataContextMapper); - -public class GridChildInitializer : IEnumerable -{ - private readonly Grid _grid; - - public GridChildInitializer(Grid grid) - { - _grid = grid; - } - - public void Add(IView item) => _grid.AddChild(item); - - public void Add(ChildWithDataContextMapper item) - => _grid.AddChild(item.Child, item.DataContextMapper); - - public IEnumerator GetEnumerator() => _grid.Children.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IChildContainer.cs b/src/Library/TerminalUI/Controls/IChildContainer.cs new file mode 100644 index 0000000..e08af62 --- /dev/null +++ b/src/Library/TerminalUI/Controls/IChildContainer.cs @@ -0,0 +1,8 @@ +using System.Collections.ObjectModel; + +namespace TerminalUI.Controls; + +public interface IChildContainer : IView +{ + ReadOnlyObservableCollection Children { get; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index df9602a..eb02ff2 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -39,8 +39,8 @@ public interface IView : IView TChild CreateChild(Func dataContextMapper) where TChild : IView, new(); - TChild AddChild(TChild child) where TChild : IView; + public TChild AddChild(TChild child) where TChild : IView; - TChild AddChild(TChild child, Func dataContextMapper) + public TChild AddChild(TChild child, Func dataContextMapper) where TChild : IView; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index 01657f5..6968b21 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -1,12 +1,12 @@ using System.Buffers; using System.Collections.ObjectModel; -using System.Security.Cryptography.X509Certificates; using DeclarativeProperty; +using PropertyChanged.SourceGenerator; using TerminalUI.Models; namespace TerminalUI.Controls; -public class ListView : View +public partial class ListView : View { private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared; @@ -18,6 +18,8 @@ public class ListView : View private int _selectedIndex = 0; private int _renderStartIndex = 0; private Size _requestedItemSize = new(0, 0); + [Notify] private int _listPadding = 0; + [Notify] private Orientation _orientation = Orientation.Vertical; public int SelectedIndex { @@ -28,7 +30,34 @@ public class ListView : View { _selectedIndex = value; OnPropertyChanged(); - ApplicationContext?.EventLoop.RequestRerender(); + OnPropertyChanged(nameof(SelectedItem)); + } + } + } + + public TItem? SelectedItem + { + get => _listViewItems is null ? default : _listViewItems[_selectedIndex].DataContext; + set + { + if (_listViewItems is null || value is null) return; + + var newSelectedIndex = -1; + for (var i = 0; i < _listViewItemLength; i++) + { + var dataContext = _listViewItems[i].DataContext; + if (dataContext is null) continue; + + if (dataContext.Equals(value)) + { + newSelectedIndex = i; + break; + } + } + + if (newSelectedIndex != -1) + { + SelectedIndex = newSelectedIndex; } } } @@ -76,60 +105,87 @@ public class ListView : View _listViewItems = null; } + _renderStartIndex = 0; + SelectedIndex = 0; OnPropertyChanged(); } } public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; + public ListView() + { + RerenderProperties.Add(nameof(ItemsSource)); + RerenderProperties.Add(nameof(SelectedIndex)); + RerenderProperties.Add(nameof(Orientation)); + } + public override Size GetRequestedSize() { - if (_listViewItems is null || _listViewItems.Length == 0) + InstantiateItemViews(); + if (_listViewItems is null || _listViewItemLength == 0) return new Size(0, 0); - var itemSize = _listViewItems[0].GetRequestedSize(); _requestedItemSize = itemSize; - return itemSize with {Height = itemSize.Height * _listViewItems.Length}; + return itemSize with {Height = itemSize.Height * _listViewItemLength}; } protected override void DefaultRenderer(Position position, Size size) { + var requestedItemSize = _requestedItemSize; + if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0) + return; + var listViewItems = InstantiateItemViews(); if (listViewItems.Length == 0) return; - var requestedItemSize = _requestedItemSize; - var itemsToRender = listViewItems.Length; var heightNeeded = requestedItemSize.Height * listViewItems.Length; var renderStartIndex = _renderStartIndex; - if (heightNeeded < size.Height) + if (heightNeeded > size.Height) { var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height); - if (SelectedIndex < renderStartIndex) + itemsToRender = maxItemsToRender; + + if (SelectedIndex - ListPadding < renderStartIndex) { - renderStartIndex = SelectedIndex - 1; + renderStartIndex = SelectedIndex - ListPadding; } - else if (SelectedIndex > renderStartIndex + maxItemsToRender) + else if (SelectedIndex + ListPadding >= renderStartIndex + maxItemsToRender) { - renderStartIndex = SelectedIndex - maxItemsToRender + 1; + renderStartIndex = SelectedIndex + ListPadding - maxItemsToRender + 1; } - - if(renderStartIndex < 0) + + if (renderStartIndex + itemsToRender > listViewItems.Length) + renderStartIndex = listViewItems.Length - itemsToRender; + + if (renderStartIndex < 0) renderStartIndex = 0; - else if (renderStartIndex + maxItemsToRender > listViewItems.Length) - renderStartIndex = listViewItems.Length - maxItemsToRender; _renderStartIndex = renderStartIndex; } var deltaY = 0; - for (var i = renderStartIndex; i < itemsToRender && i < listViewItems.Length; i++) + var lastItemIndex = renderStartIndex + itemsToRender; + if (lastItemIndex > listViewItems.Length) + lastItemIndex = listViewItems.Length; + + for (var i = renderStartIndex; i < lastItemIndex; i++) { var item = listViewItems[i]; - item.Render(position with {Y = position.Y + deltaY}, requestedItemSize); + item.Render(position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width}); deltaY += requestedItemSize.Height; } + + var driver = ApplicationContext!.ConsoleDriver; + var placeholder = new string(' ', size.Width); + driver.ResetColor(); + for (var i = deltaY; i < size.Height; i++) + { + driver.SetCursorPosition(position with {Y = position.Y + i}); + driver.Write(placeholder); + } } private Span> InstantiateItemViews() diff --git a/src/Library/TerminalUI/Controls/StackPanel.cs b/src/Library/TerminalUI/Controls/StackPanel.cs new file mode 100644 index 0000000..2ee4e3e --- /dev/null +++ b/src/Library/TerminalUI/Controls/StackPanel.cs @@ -0,0 +1,54 @@ +using System.Collections.ObjectModel; +using PropertyChanged.SourceGenerator; +using TerminalUI.Models; + +namespace TerminalUI.Controls; + +public partial class StackPanel : ChildContainerView +{ + private readonly Dictionary _requestedSizes = new(); + [Notify] private Orientation _orientation = Orientation.Vertical; + + public override Size GetRequestedSize() + { + _requestedSizes.Clear(); + var width = 0; + var height = 0; + + foreach (var child in Children) + { + var childSize = child.GetRequestedSize(); + _requestedSizes.Add(child, childSize); + + if (Orientation == Orientation.Vertical) + { + width = Math.Max(width, childSize.Width); + height += childSize.Height; + } + else + { + width += childSize.Width; + height = Math.Max(height, childSize.Height); + } + } + + return new Size(width, height); + } + + protected override void DefaultRenderer(Position position, Size size) + { + var delta = 0; + foreach (var child in Children) + { + if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found"); + var childPosition = Orientation == Orientation.Vertical + ? position with {Y = position.Y + delta} + : position with {X = position.X + delta}; + child.Render(childPosition, childSize); + + delta += Orientation == Orientation.Vertical + ? childSize.Height + : childSize.Width; + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index b380e44..d392a2c 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -14,6 +14,7 @@ public partial class TextBlock : View [Notify] private string? _text = string.Empty; [Notify] private IColor? _foreground; [Notify] private IColor? _background; + [Notify] private TextAlignment _textAlignment = TextAlignment.Left; public TextBlock() { @@ -26,12 +27,15 @@ public partial class TextBlock : View RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Foreground)); RerenderProperties.Add(nameof(Background)); + RerenderProperties.Add(nameof(TextAlignment)); } public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1); protected override void DefaultRenderer(Position position, Size size) { + if (size.Width == 0 || size.Height == 0) return; + var driver = ApplicationContext!.ConsoleDriver; var renderContext = new RenderContext(position, Text, _foreground, _background); if (!NeedsRerender(renderContext)) return; @@ -52,7 +56,17 @@ public partial class TextBlock : View driver.SetBackgroundColor(background); } - driver.Write(Text); + var text = TextAlignment switch + { + TextAlignment.Right => string.Format($"{{0,{size.Width}}}", Text), + _ => string.Format($"{{0,{-size.Width}}}", Text) + }; + if (text.Length > size.Width) + { + text = text[..size.Width]; + } + + driver.Write(text); } private bool NeedsRerender(RenderContext renderContext) diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index f32f292..c1328d3 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -16,6 +16,7 @@ public abstract partial class View : IView [Notify] private int? _minHeight; [Notify] private int? _maxHeight; [Notify] private int? _height; + [Notify] private IApplicationContext? _applicationContext; private bool _attached; public bool Attached @@ -33,7 +34,6 @@ public abstract partial class View : IView } public List Extensions { get; } = new(); public Action RenderMethod { get; set; } - public IApplicationContext? ApplicationContext { get; set; } public event Action? Disposed; protected List RerenderProperties { get; } = new(); diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index 60073f7..c11b8cc 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -41,14 +41,14 @@ public class EventLoop : IEventLoop { if (!_rerenderRequested) return; _rerenderRequested = false; - viewsToRender = _viewsToRender.ToList(); } - var size =_applicationContext.ConsoleDriver.GetBufferSize(); + var size = _applicationContext.ConsoleDriver.GetWindowSize(); foreach (var view in viewsToRender) { view.Attached = true; + view.GetRequestedSize(); view.Render(new Position(0, 0), size); } } diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/Binding.cs index 09c9662..3051221 100644 --- a/src/Library/TerminalUI/Extensions/Binding.cs +++ b/src/Library/TerminalUI/Extensions/Binding.cs @@ -6,20 +6,44 @@ namespace TerminalUI.Extensions; public static class Binding { - public static Binding Bind( + public static Binding Bind( this TView targetView, IView dataSourceView, Expression> dataContextExpression, - Expression> propertyExpression) + Expression> propertyExpression, + TResult? fallbackValue = default) { if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo}) throw new AggregateException(nameof(propertyExpression) + " must be a property expression"); - return new Binding( - dataSourceView, - dataContextExpression, - targetView, - propertyInfo + return new Binding( + dataSourceView, + dataContextExpression, + targetView, + propertyInfo, + value => value, + fallbackValue + ); + } + + public static Binding Bind( + this TView targetView, + IView dataSourceView, + Expression> dataContextExpression, + Expression> propertyExpression, + Func converter, + TResult? fallbackValue = default) + { + if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo}) + throw new AggregateException(nameof(propertyExpression) + " must be a property expression"); + + return new Binding( + dataSourceView, + dataContextExpression, + targetView, + propertyInfo, + converter, + fallbackValue ); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Extensions/ViewExtensions.cs b/src/Library/TerminalUI/Extensions/ViewExtensions.cs index 1523ada..4380d9a 100644 --- a/src/Library/TerminalUI/Extensions/ViewExtensions.cs +++ b/src/Library/TerminalUI/Extensions/ViewExtensions.cs @@ -11,4 +11,10 @@ public static class ViewExtensions this IView view, Func dataContextMapper) => new(view, dataContextMapper); + + public static TView Setup(this TView view, Action action) + { + action(view); + return view; + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index 9a26400..382edff 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -1,4 +1,5 @@ -using TerminalUI.ConsoleDrivers; +using Microsoft.Extensions.Logging; +using TerminalUI.ConsoleDrivers; namespace TerminalUI; @@ -7,4 +8,5 @@ public interface IApplicationContext IEventLoop EventLoop { get; init; } bool IsRunning { get; set; } IConsoleDriver ConsoleDriver { get; init; } + ILoggerFactory? LoggerFactory { get; init; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Option.cs b/src/Library/TerminalUI/Models/Option.cs new file mode 100644 index 0000000..3f8023f --- /dev/null +++ b/src/Library/TerminalUI/Models/Option.cs @@ -0,0 +1,13 @@ +namespace TerminalUI.Models; + +public readonly ref struct Option +{ + public readonly T Value; + public readonly bool IsSome; + + public Option(T value, bool isSome) + { + Value = value; + IsSome = isSome; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Orientation.cs b/src/Library/TerminalUI/Models/Orientation.cs new file mode 100644 index 0000000..7a0b1b6 --- /dev/null +++ b/src/Library/TerminalUI/Models/Orientation.cs @@ -0,0 +1,7 @@ +namespace TerminalUI.Models; + +public enum Orientation +{ + Horizontal, + Vertical +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Size.cs b/src/Library/TerminalUI/Models/Size.cs index 9be9239..82d3767 100644 --- a/src/Library/TerminalUI/Models/Size.cs +++ b/src/Library/TerminalUI/Models/Size.cs @@ -1,3 +1,3 @@ namespace TerminalUI.Models; -public record Size(int Width, int Height); \ No newline at end of file +public record struct Size(int Width, int Height); \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/TextAlignment.cs b/src/Library/TerminalUI/Models/TextAlignment.cs new file mode 100644 index 0000000..16b1d5a --- /dev/null +++ b/src/Library/TerminalUI/Models/TextAlignment.cs @@ -0,0 +1,8 @@ +namespace TerminalUI.Models; + +public enum TextAlignment +{ + Left, + Center, + Right +} \ No newline at end of file diff --git a/src/Library/TerminalUI/TerminalUI.csproj b/src/Library/TerminalUI/TerminalUI.csproj index c5e3ac5..b0374e1 100644 --- a/src/Library/TerminalUI/TerminalUI.csproj +++ b/src/Library/TerminalUI/TerminalUI.csproj @@ -11,6 +11,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Library/TerminalUI/ViewExtensions/GridPositionExtension.cs b/src/Library/TerminalUI/ViewExtensions/GridPositionExtension.cs index a4e2fc6..08dbf96 100644 --- a/src/Library/TerminalUI/ViewExtensions/GridPositionExtension.cs +++ b/src/Library/TerminalUI/ViewExtensions/GridPositionExtension.cs @@ -1,3 +1,3 @@ namespace TerminalUI.ViewExtensions; -public record GridPositionExtension(int Row, int Column); \ No newline at end of file +public record GridPositionExtension(int Column, int Row); \ No newline at end of file